Merge remote-tracking branch 'upstream/master' into feature/color-accessibility-ui

Conflicts:
	src/components/App.jsx
	src/styles/_components.scss
This commit is contained in:
orangemug 2018-09-23 11:39:15 +01:00
commit 8f07a79a49
63 changed files with 666 additions and 663 deletions

View file

@ -5,7 +5,7 @@
"test": { "test": {
"plugins": [ "plugins": [
["istanbul", { ["istanbul", {
exclude: ["node_modules/**", "test/**"] "exclude": ["node_modules/**", "test/**"]
}] }]
] ]
} }

View file

@ -1,22 +0,0 @@
language: node_js
matrix:
include:
- os: osx
node_js: "6"
- os: osx
node_js: "8"
- os: osx
node_js: "9"
install:
- npm install
script:
- mkdir public
- node --stack_size=100000 $(which npm) run build
- npm run lint
- npm run lint-styles
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8

View file

@ -22,6 +22,11 @@ targeted at developers and map designers.
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independance is an OSS map designer. Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independance is an OSS map designer.
## Donations
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
## Documentation ## Documentation
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate! The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
@ -157,6 +162,6 @@ Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors. Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
**Disclaimer** This project is not affiliated with Mapbox or Mapbox Studio. It is a independent style editor for the **Disclaimer** This project is not affiliated with Mapbox or Mapbox Studio. It is an independent style editor for the
open source technology in the Mapbox GL ecosystem. open source technology in the Mapbox GL ecosystem.
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor. As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.

View file

@ -1,8 +1,9 @@
image: Visual Studio 2017
environment: environment:
matrix: matrix:
- nodejs_version: "6"
- nodejs_version: "8" - nodejs_version: "8"
- nodejs_version: "9" - nodejs_version: "9"
- nodejs_version: "10"
platform: platform:
- x86 - x86
- x64 - x64

View file

@ -14,25 +14,6 @@ var OUTPATH = artifacts.pathSync("/build");
module.exports = { module.exports = {
entry: { entry: {
app: './src/index.jsx', app: './src/index.jsx',
vendor: [
'file-saver',
'mapbox-gl/dist/mapbox-gl.js',
"lodash.clonedeep",
"lodash.throttle",
'color',
'react',
"react-dom",
"react-color",
"react-file-reader-input",
"react-collapse",
"react-height",
"react-icon-base",
"react-motion",
"react-sortable-hoc",
"request",
//TODO: Icons raise multi vendor errors?
//"react-icons",
]
}, },
output: { output: {
path: OUTPATH, path: OUTPATH,
@ -55,7 +36,6 @@ module.exports = {
}, },
plugins: [ plugins: [
new webpack.NoEmitOnErrorsPlugin(), new webpack.NoEmitOnErrorsPlugin(),
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: '[chunkhash].vendor.js' }),
new WebpackCleanupPlugin(), new WebpackCleanupPlugin(),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': { 'process.env': {

86
package-lock.json generated
View file

@ -1,6 +1,6 @@
{ {
"name": "maputnik", "name": "maputnik",
"version": "1.2.0", "version": "1.5.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -281,25 +281,20 @@
"wgs84": "0.0.0" "wgs84": "0.0.0"
} }
}, },
"@mapbox/gl-matrix": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@mapbox/gl-matrix/-/gl-matrix-0.0.1.tgz",
"integrity": "sha1-5RJqq01kw2uBx6l9CuDd3eV3PSs="
},
"@mapbox/jsonlint-lines-primitives": { "@mapbox/jsonlint-lines-primitives": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.1.tgz",
"integrity": "sha512-LGegvJq+ks4UFnEAvtAhygFRZxaNg2vw7PsvLOAXY8ziJLv5aFDSdHDEMJA/Q8PG5dNhMioUvo0kNqb7U5dPoQ==" "integrity": "sha512-LGegvJq+ks4UFnEAvtAhygFRZxaNg2vw7PsvLOAXY8ziJLv5aFDSdHDEMJA/Q8PG5dNhMioUvo0kNqb7U5dPoQ=="
}, },
"@mapbox/mapbox-gl-rtl-text": { "@mapbox/mapbox-gl-rtl-text": {
"version": "0.1.2", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-rtl-text/-/mapbox-gl-rtl-text-0.1.2.tgz", "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-rtl-text/-/mapbox-gl-rtl-text-0.2.0.tgz",
"integrity": "sha512-x5xSHNAD5MeuasbEpGyDBGy4zPQysDdvDUlnjDKMxic8KqmcxcCP3ojlIaqEWYFvMmq7FKyWIF7K8jO4bHavKQ==" "integrity": "sha512-oijFgP0DTMRtLzEpZM5lCrlagoQaOXUhZOMKOXaCF56QohKdRO4uu8RUKcuvfpWz6ruQcyH2yikBl09aWh3Duw=="
}, },
"@mapbox/mapbox-gl-style-spec": { "@mapbox/mapbox-gl-style-spec": {
"version": "12.0.0", "version": "13.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-12.0.0.tgz", "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.1.0.tgz",
"integrity": "sha1-dRW/rbs6hZ66HFbZn8E4P6YpJ0c=", "integrity": "sha512-WoviOAN39bZ3Fm3jBycGZg64rAihZ1ykVOR/XAiJLPviyPGTSWo0kIzcW2FRg23LJ0FSysEXhbDWXtHADUFj+w==",
"requires": { "requires": {
"@mapbox/jsonlint-lines-primitives": "2.0.1", "@mapbox/jsonlint-lines-primitives": "2.0.1",
"@mapbox/unitbezier": "0.0.0", "@mapbox/unitbezier": "0.0.0",
@ -309,11 +304,6 @@
"sort-object": "0.3.2" "sort-object": "0.3.2"
} }
}, },
"@mapbox/mapbox-gl-supported": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.3.1.tgz",
"integrity": "sha512-tMdtbKjxVaA3WZubFTKIPjAHKNz8PCqUGdRgT7RIpqLHLxZi3eQWzixrnyYA5dglEnbfcIEPvtRQu/4yXPt2YQ=="
},
"@mapbox/point-geometry": { "@mapbox/point-geometry": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
@ -6536,11 +6526,6 @@
} }
} }
}, },
"geojson-vt": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.1.1.tgz",
"integrity": "sha512-Idr9VfV+EtycnleWLKgYbTFAthfE1V700eCqTlC6P2ODh715fTp4cCQYleYh3TPLzlzn2K4Gk514TstsoDue9w=="
},
"get-caller-file": { "get-caller-file": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
@ -6584,6 +6569,11 @@
"utf8": "2.1.2" "utf8": "2.1.2"
} }
}, },
"gl-matrix": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-2.7.1.tgz",
"integrity": "sha512-22I6q7aO2oKNahNV0+9JavVNUhQXRTvR5jP2s8U1l93TkjcQe8RK6MeMYpM7+66R0sCVUgSdO97BL439vePyzQ=="
},
"glob": { "glob": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
@ -8216,11 +8206,6 @@
} }
} }
}, },
"kdbush": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-1.0.1.tgz",
"integrity": "sha1-PL0D6d6tnA9vZszblkUOXOzGQOA="
},
"kew": { "kew": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz",
@ -8582,13 +8567,12 @@
} }
}, },
"mapbox-gl": { "mapbox-gl": {
"version": "0.45.0", "version": "0.47.0",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-0.45.0.tgz", "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-0.47.0.tgz",
"integrity": "sha1-r3HMgk8NflHM1cUF6q5BG8CRDM0=", "integrity": "sha512-y1AlNYMAKaqEtaqni0zOMYj9gTc1gZ0lqLkxXK9iFg5+ZBITc5DL9AcrXhpEXNxUzXKFa7dZkSULyNaqXFQ8yQ==",
"requires": { "requires": {
"@mapbox/gl-matrix": "0.0.1",
"@mapbox/jsonlint-lines-primitives": "2.0.1", "@mapbox/jsonlint-lines-primitives": "2.0.1",
"@mapbox/mapbox-gl-supported": "1.3.1", "@mapbox/mapbox-gl-supported": "1.4.0",
"@mapbox/point-geometry": "0.1.0", "@mapbox/point-geometry": "0.1.0",
"@mapbox/shelf-pack": "3.1.0", "@mapbox/shelf-pack": "3.1.0",
"@mapbox/tiny-sdf": "1.1.0", "@mapbox/tiny-sdf": "1.1.0",
@ -8599,7 +8583,8 @@
"csscolorparser": "1.0.3", "csscolorparser": "1.0.3",
"earcut": "2.1.3", "earcut": "2.1.3",
"geojson-rewind": "0.3.1", "geojson-rewind": "0.3.1",
"geojson-vt": "3.1.1", "geojson-vt": "3.2.0",
"gl-matrix": "2.7.1",
"gray-matter": "3.1.1", "gray-matter": "3.1.1",
"grid-index": "1.0.0", "grid-index": "1.0.0",
"minimist": "0.0.8", "minimist": "0.0.8",
@ -8608,10 +8593,35 @@
"rw": "1.3.3", "rw": "1.3.3",
"shuffle-seed": "1.1.6", "shuffle-seed": "1.1.6",
"sort-object": "0.3.2", "sort-object": "0.3.2",
"supercluster": "2.3.0", "supercluster": "4.1.1",
"through2": "2.0.3", "through2": "2.0.3",
"tinyqueue": "1.2.3", "tinyqueue": "1.2.3",
"vt-pbf": "3.1.0" "vt-pbf": "3.1.0"
},
"dependencies": {
"@mapbox/mapbox-gl-supported": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.4.0.tgz",
"integrity": "sha512-ZD0Io4XK+/vU/4zpANjOtdWfVszAgnaMPsGR6LKsWh4kLIEv9qoobTVmJPPuwuM+ZI2b3BlZ6DYw1XHVmv6YTA=="
},
"geojson-vt": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.0.tgz",
"integrity": "sha512-qk7sEv7dMfuGzflwClsgtO1fWPut/TqCInWEEUJc/Ofn4tmqBGznnPv3eUdxtwMkulMaAwSL3osHiyN03XJd/w=="
},
"kdbush": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-2.0.1.tgz",
"integrity": "sha512-9KqSdmWCkBIisFIGclT0FRagKhI7IVbMyUjsxCFG0Ly1Dg6whlxJ7b9lrq8ifk3X/fGeJzok1R75LQfZTfA5zQ=="
},
"supercluster": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-4.1.1.tgz",
"integrity": "sha512-sF0FfUOPFp96DKzwWFLeQOEqqKu2PpcesxAFeFsknA/q7g7igVVn/p3NI2XHEghNSyDAqunKNKqAbqNO8+7NDQ==",
"requires": {
"kdbush": "2.0.1"
}
}
} }
}, },
"mapbox-gl-inspect": { "mapbox-gl-inspect": {
@ -14176,14 +14186,6 @@
} }
} }
}, },
"supercluster": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-2.3.0.tgz",
"integrity": "sha1-h6tWCBu+qaHXJN9TUe6ejDry9Is=",
"requires": {
"kdbush": "1.0.1"
}
},
"supports-color": { "supports-color": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",

View file

@ -1,6 +1,6 @@
{ {
"name": "maputnik", "name": "maputnik",
"version": "1.2.0", "version": "1.5.0",
"description": "A MapboxGL visual style editor", "description": "A MapboxGL visual style editor",
"main": "''", "main": "''",
"scripts": { "scripts": {
@ -10,8 +10,7 @@
"test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch", "test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch",
"start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js", "start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js",
"lint": "eslint --ext js --ext jsx {src,test}", "lint": "eslint --ext js --ext jsx {src,test}",
"lint-styles": "stylelint 'src/styles/*.scss'", "lint-styles": "stylelint 'src/styles/*.scss'"
"nsp": "nsp check --reporter summary"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -21,44 +20,39 @@
"license": "MIT", "license": "MIT",
"homepage": "https://github.com/maputnik/editor#readme", "homepage": "https://github.com/maputnik/editor#readme",
"dependencies": { "dependencies": {
"@mapbox/mapbox-gl-rtl-text": "^0.1.2", "@mapbox/mapbox-gl-rtl-text": "^0.2.0",
"@mapbox/mapbox-gl-style-spec": "^12.0.0", "@mapbox/mapbox-gl-style-spec": "^13.1.0",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"codemirror": "^5.37.0", "codemirror": "^5.37.0",
"color": "^3.0.0", "color": "^3.0.0",
"file-saver": "^1.3.8", "file-saver": "^1.3.8",
"github-api": "^3.0.0",
"jsonlint": "github:josdejong/jsonlint#85a19d7", "jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash.capitalize": "^4.2.1", "lodash.capitalize": "^4.2.1",
"lodash.clamp": "^4.0.3", "lodash.clamp": "^4.0.3",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"mapbox-gl": "^0.45.0", "mapbox-gl": "^0.47.0",
"mapbox-gl-inspect": "^1.3.1", "mapbox-gl-inspect": "^1.3.1",
"maputnik-design": "github:maputnik/design", "maputnik-design": "github:maputnik/design",
"mousetrap": "^1.6.1",
"ol-mapbox-style": "^2.10.1", "ol-mapbox-style": "^2.10.1",
"ol": "^4.6.5", "ol": "^4.6.5",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"react": "^16.3.2", "react": "^16.3.2",
"react-addons-pure-render-mixin": "^15.6.2",
"react-aria-menubutton": "^5.1.1", "react-aria-menubutton": "^5.1.1",
"react-aria-modal": "^2.12.1", "react-aria-modal": "^2.12.1",
"react-autobind": "^1.0.6",
"react-autocomplete": "^1.7.2", "react-autocomplete": "^1.7.2",
"react-codemirror2": "^4.2.1", "react-codemirror2": "^4.2.1",
"react-collapse": "^4.0.3", "react-collapse": "^4.0.3",
"react-color": "^2.14.1", "react-color": "^2.14.1",
"react-copy-to-clipboard": "^5.0.1",
"react-dom": "^16.3.2", "react-dom": "^16.3.2",
"react-file-reader-input": "^1.1.4", "react-file-reader-input": "^1.1.4",
"react-height": "^3.0.0",
"react-icon-base": "^2.1.1", "react-icon-base": "^2.1.1",
"react-icons": "^2.2.7", "react-icons": "^2.2.7",
"react-motion": "^0.5.2", "react-motion": "^0.5.2",
"react-sortable-hoc": "^0.6.8", "react-sortable-hoc": "^0.6.8",
"reconnecting-websocket": "^3.2.2", "reconnecting-websocket": "^3.2.2",
"request": "^2.85.0",
"url": "^0.11.0" "url": "^0.11.0"
}, },
"jshintConfig": { "jshintConfig": {
@ -117,7 +111,6 @@
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
"babel-register": "^6.26.0", "babel-register": "^6.26.0",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"base64-loader": "^1.0.0",
"copy-webpack-plugin": "^4.5.1", "copy-webpack-plugin": "^4.5.1",
"cors": "^2.8.4", "cors": "^2.8.4",
"cross-env": "^5.1.4", "cross-env": "^5.1.4",
@ -135,7 +128,7 @@
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mocha": "^5.1.1", "mocha": "^5.1.1",
"node-sass": "^4.9.0", "node-sass": "^4.9.0",
"nsp": "^3.1.0", "raw-loader": "^0.5.1",
"react-hot-loader": "^3.1.1", "react-hot-loader": "^3.1.1",
"sass-loader": "^7.0.1", "sass-loader": "^7.0.1",
"selenium-standalone": "^6.14.0", "selenium-standalone": "^6.14.0",
@ -147,7 +140,6 @@
"uglifyjs-webpack-plugin": "^1.2.4", "uglifyjs-webpack-plugin": "^1.2.4",
"uuid": "^3.1.0", "uuid": "^3.1.0",
"wdio-mocha-framework": "^0.5.13", "wdio-mocha-framework": "^0.5.13",
"wdio-phantomjs-service": "^0.2.2",
"wdio-selenium-standalone-service": "0.0.10", "wdio-selenium-standalone-service": "0.0.10",
"wdio-spec-reporter": "^0.1.2", "wdio-spec-reporter": "^0.1.2",
"webdriverio": "^4.12.0", "webdriverio": "^4.12.0",

View file

@ -1,12 +1,11 @@
import autoBind from 'react-autobind';
import React from 'react' import React from 'react'
import Mousetrap from 'mousetrap'
import cloneDeep from 'lodash.clonedeep' import cloneDeep from 'lodash.clonedeep'
import clamp from 'lodash.clamp' import clamp from 'lodash.clamp'
import {arrayMove} from 'react-sortable-hoc'; import {arrayMove} from 'react-sortable-hoc'
import url from 'url' import url from 'url'
import MapboxGlMap from './map/MapboxGlMap' import MapboxGlMap from './map/MapboxGlMap'
import OpenLayers3Map from './map/OpenLayers3Map'
import LayerList from './layers/LayerList' import LayerList from './layers/LayerList'
import LayerEditor from './layers/LayerEditor' import LayerEditor from './layers/LayerEditor'
import Toolbar from './Toolbar' import Toolbar from './Toolbar'
@ -18,13 +17,14 @@ import ExportModal from './modals/ExportModal'
import SourcesModal from './modals/SourcesModal' import SourcesModal from './modals/SourcesModal'
import OpenModal from './modals/OpenModal' import OpenModal from './modals/OpenModal'
import ShortcutsModal from './modals/ShortcutsModal' import ShortcutsModal from './modals/ShortcutsModal'
import SurveyModal from './modals/SurveyModal'
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata' import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import style from '../libs/style.js' import style from '../libs/style'
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen' import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
import { undoMessages, redoMessages } from '../libs/diffmessage' import { undoMessages, redoMessages } from '../libs/diffmessage'
import { loadDefaultStyle, StyleStore } from '../libs/stylestore' import { StyleStore } from '../libs/stylestore'
import { ApiStyleStore } from '../libs/apistore' import { ApiStyleStore } from '../libs/apistore'
import { RevisionStore } from '../libs/revisions' import { RevisionStore } from '../libs/revisions'
import LayerWatcher from '../libs/layerwatcher' import LayerWatcher from '../libs/layerwatcher'
@ -34,7 +34,7 @@ import Debug from '../libs/debug'
import queryUtil from '../libs/query-util' import queryUtil from '../libs/query-util'
import MapboxGl from 'mapbox-gl' import MapboxGl from 'mapbox-gl'
import mapboxUtil from 'mapbox-gl/src/util/mapbox' import { normalizeSourceURL } from 'mapbox-gl/src/util/mapbox'
function updateRootSpec(spec, fieldName, newValues) { function updateRootSpec(spec, fieldName, newValues) {
@ -53,6 +53,8 @@ function updateRootSpec(spec, fieldName, newValues) {
export default class App extends React.Component { export default class App extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
autoBind(this);
this.revisionStore = new RevisionStore() this.revisionStore = new RevisionStore()
this.styleStore = new ApiStyleStore({ this.styleStore = new ApiStyleStore({
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false) onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false)
@ -172,9 +174,11 @@ export default class App extends React.Component {
open: false, open: false,
shortcuts: false, shortcuts: false,
export: false, export: false,
survey: localStorage.hasOwnProperty('survey') ? false : true
}, },
mapOptions: { mapOptions: {
showTileBoundaries: queryUtil.asBool(queryObj, "show-tile-boundaries") showTileBoundaries: queryUtil.asBool(queryObj, "show-tile-boundaries"),
showCollisionBoxes: queryUtil.asBool(queryObj, "show-collision-boxes")
}, },
} }
@ -183,14 +187,31 @@ export default class App extends React.Component {
}) })
} }
handleKeyPress(e) {
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
this.onRedo(e);
}
else if(e.metaKey && e.keyCode === 90) {
this.onUndo(e);
}
}
else {
if(e.ctrlKey && e.keyCode === 90) {
this.onUndo(e);
}
else if(e.ctrlKey && e.keyCode === 89) {
this.onRedo(e);
}
}
}
componentDidMount() { componentDidMount() {
Mousetrap.bind(['mod+z'], this.onUndo.bind(this)); window.addEventListener("keydown", this.handleKeyPress);
Mousetrap.bind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
} }
componentWillUnmount() { componentWillUnmount() {
Mousetrap.unbind(['mod+z'], this.onUndo.bind(this)); window.removeEventListener("keydown", this.handleKeyPress);
Mousetrap.unbind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
} }
saveStyle(snapshotStyle) { saveStyle(snapshotStyle) {
@ -213,7 +234,7 @@ export default class App extends React.Component {
}) })
} }
onStyleChanged(newStyle, save=true) { onStyleChanged = (newStyle, save=true) => {
const errors = styleSpec.validate(newStyle, styleSpec.latest) const errors = styleSpec.validate(newStyle, styleSpec.latest)
if(errors.length === 0) { if(errors.length === 0) {
@ -240,7 +261,7 @@ export default class App extends React.Component {
this.fetchSources(); this.fetchSources();
} }
onUndo() { onUndo = () => {
const activeStyle = this.revisionStore.undo() const activeStyle = this.revisionStore.undo()
const messages = undoMessages(this.state.mapStyle, activeStyle) const messages = undoMessages(this.state.mapStyle, activeStyle)
this.saveStyle(activeStyle) this.saveStyle(activeStyle)
@ -250,7 +271,7 @@ export default class App extends React.Component {
}) })
} }
onRedo() { onRedo = () => {
const activeStyle = this.revisionStore.redo() const activeStyle = this.revisionStore.redo()
const messages = redoMessages(this.state.mapStyle, activeStyle) const messages = redoMessages(this.state.mapStyle, activeStyle)
this.saveStyle(activeStyle) this.saveStyle(activeStyle)
@ -260,7 +281,7 @@ export default class App extends React.Component {
}) })
} }
onMoveLayer(move) { onMoveLayer = (move) => {
let { oldIndex, newIndex } = move; let { oldIndex, newIndex } = move;
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
oldIndex = clamp(oldIndex, 0, layers.length-1); oldIndex = clamp(oldIndex, 0, layers.length-1);
@ -278,7 +299,7 @@ export default class App extends React.Component {
this.onLayersChange(layers); this.onLayersChange(layers);
} }
onLayersChange(changedLayers) { onLayersChange = (changedLayers) => {
const changedStyle = { const changedStyle = {
...this.state.mapStyle, ...this.state.mapStyle,
layers: changedLayers layers: changedLayers
@ -286,7 +307,7 @@ export default class App extends React.Component {
this.onStyleChanged(changedStyle) this.onStyleChanged(changedStyle)
} }
onLayerDestroy(layerId) { onLayerDestroy = (layerId) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const remainingLayers = layers.slice(0); const remainingLayers = layers.slice(0);
const idx = style.indexOfLayer(remainingLayers, layerId) const idx = style.indexOfLayer(remainingLayers, layerId)
@ -294,7 +315,7 @@ export default class App extends React.Component {
this.onLayersChange(remainingLayers); this.onLayersChange(remainingLayers);
} }
onLayerCopy(layerId) { onLayerCopy = (layerId) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0) const changedLayers = layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layerId) const idx = style.indexOfLayer(changedLayers, layerId)
@ -305,7 +326,7 @@ export default class App extends React.Component {
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerVisibilityToggle(layerId) { onLayerVisibilityToggle = (layerId) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0) const changedLayers = layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layerId) const idx = style.indexOfLayer(changedLayers, layerId)
@ -320,7 +341,7 @@ export default class App extends React.Component {
} }
onLayerIdChange(oldId, newId) { onLayerIdChange = (oldId, newId) => {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.mapStyle.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, oldId) const idx = style.indexOfLayer(changedLayers, oldId)
@ -332,7 +353,7 @@ export default class App extends React.Component {
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerChanged(layer) { onLayerChanged = (layer) => {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.mapStyle.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layer.id) const idx = style.indexOfLayer(changedLayers, layer.id)
changedLayers[idx] = layer changedLayers[idx] = layer
@ -340,7 +361,7 @@ export default class App extends React.Component {
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
setMapState(newState) { setMapState = (newState) => {
this.setState({ this.setState({
mapState: newState mapState: newState
}) })
@ -362,12 +383,14 @@ export default class App extends React.Component {
if(!this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url")) { if(!this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url")) {
let url = val.url; let url = val.url;
try { try {
url = mapboxUtil.normalizeSourceURL(url, MapboxGl.accessToken); url = normalizeSourceURL(url, MapboxGl.accessToken);
} catch(err) { } catch(err) {
console.warn("Failed to normalizeSourceURL: ", err); console.warn("Failed to normalizeSourceURL: ", err);
} }
fetch(url) fetch(url, {
mode: 'cors',
})
.then((response) => { .then((response) => {
return response.json(); return response.json();
}) })
@ -404,7 +427,7 @@ export default class App extends React.Component {
mapRenderer() { mapRenderer() {
const mapProps = { const mapProps = {
mapStyle: style.replaceAccessToken(this.state.mapStyle, {allowFallback: true}), mapStyle: style.replaceAccessTokens(this.state.mapStyle, {allowFallback: true}),
options: this.state.mapOptions, options: this.state.mapOptions,
onDataChange: (e) => { onDataChange: (e) => {
this.layerWatcher.analyzeMap(e.map) this.layerWatcher.analyzeMap(e.map)
@ -419,28 +442,29 @@ export default class App extends React.Component {
// Check if OL3 code has been loaded? // Check if OL3 code has been loaded?
if(renderer === 'ol3') { if(renderer === 'ol3') {
mapElement = <OpenLayers3Map {...mapProps} /> mapElement = <div>TODO</div>
} else { } else {
mapElement = <MapboxGlMap {...mapProps} mapElement = <MapboxGlMap {...mapProps}
inspectModeEnabled={this.state.mapState === "inspect"} inspectModeEnabled={this.state.mapState === "inspect"}
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]} highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
onLayerSelect={this.onLayerSelect.bind(this)} /> onLayerSelect={this.onLayerSelect} />
} }
let filterName = ""; let filterName;
if(this.state.mapState.match(/^filter-/)) { if(this.state.mapState.match(/^filter-/)) {
filterName = this.state.mapState.replace(/^filter-/, ""); filterName = this.state.mapState.replace(/^filter-/, "");
} }
const elementStyle = { const elementStyle = {};
"filter": `url('#${filterName}')` if (filterName) {
elementStyle.filter = `url('color-accessibility.svg#${filterName}')`;
}; };
return <div style={elementStyle}> return <div style={elementStyle} className="maputnik-map__container">
{mapElement} {mapElement}
</div> </div>
} }
onLayerSelect(layerId) { onLayerSelect = (layerId) => {
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId) const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId)
this.setState({ selectedLayerIndex: idx }) this.setState({ selectedLayerIndex: idx })
} }
@ -452,6 +476,10 @@ export default class App extends React.Component {
[modalName]: !this.state.isOpen[modalName] [modalName]: !this.state.isOpen[modalName]
} }
}) })
if(modalName === 'survey') {
localStorage.setItem('survey', '');
}
} }
render() { render() {
@ -464,19 +492,19 @@ export default class App extends React.Component {
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
inspectModeEnabled={this.state.mapState === "inspect"} inspectModeEnabled={this.state.mapState === "inspect"}
sources={this.state.sources} sources={this.state.sources}
onStyleChanged={this.onStyleChanged.bind(this)} onStyleChanged={this.onStyleChanged}
onStyleOpen={this.onStyleChanged.bind(this)} onStyleOpen={this.onStyleChanged}
onSetMapState={this.setMapState.bind(this)} onSetMapState={this.setMapState}
onToggleModal={this.toggleModal.bind(this)} onToggleModal={this.toggleModal.bind(this)}
/> />
const layerList = <LayerList const layerList = <LayerList
onMoveLayer={this.onMoveLayer.bind(this)} onMoveLayer={this.onMoveLayer}
onLayerDestroy={this.onLayerDestroy.bind(this)} onLayerDestroy={this.onLayerDestroy}
onLayerCopy={this.onLayerCopy.bind(this)} onLayerCopy={this.onLayerCopy}
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)} onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayersChange={this.onLayersChange.bind(this)} onLayersChange={this.onLayersChange}
onLayerSelect={this.onLayerSelect.bind(this)} onLayerSelect={this.onLayerSelect}
selectedLayerIndex={this.state.selectedLayerIndex} selectedLayerIndex={this.state.selectedLayerIndex}
layers={layers} layers={layers}
sources={this.state.sources} sources={this.state.sources}
@ -490,12 +518,12 @@ export default class App extends React.Component {
sources={this.state.sources} sources={this.state.sources}
vectorLayers={this.state.vectorLayers} vectorLayers={this.state.vectorLayers}
spec={this.state.spec} spec={this.state.spec}
onMoveLayer={this.onMoveLayer.bind(this)} onMoveLayer={this.onMoveLayer}
onLayerChanged={this.onLayerChanged.bind(this)} onLayerChanged={this.onLayerChanged}
onLayerDestroy={this.onLayerDestroy.bind(this)} onLayerDestroy={this.onLayerDestroy}
onLayerCopy={this.onLayerCopy.bind(this)} onLayerCopy={this.onLayerCopy}
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)} onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayerIdChange={this.onLayerIdChange.bind(this)} onLayerIdChange={this.onLayerIdChange}
/> : null /> : null
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
@ -511,27 +539,31 @@ export default class App extends React.Component {
/> />
<SettingsModal <SettingsModal
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged.bind(this)} onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.settings} isOpen={this.state.isOpen.settings}
onOpenToggle={this.toggleModal.bind(this, 'settings')} onOpenToggle={this.toggleModal.bind(this, 'settings')}
/> />
<ExportModal <ExportModal
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged.bind(this)} onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.export} isOpen={this.state.isOpen.export}
onOpenToggle={this.toggleModal.bind(this, 'export')} onOpenToggle={this.toggleModal.bind(this, 'export')}
/> />
<OpenModal <OpenModal
isOpen={this.state.isOpen.open} isOpen={this.state.isOpen.open}
onStyleOpen={this.onStyleChanged.bind(this)} onStyleOpen={this.onStyleChanged}
onOpenToggle={this.toggleModal.bind(this, 'open')} onOpenToggle={this.toggleModal.bind(this, 'open')}
/> />
<SourcesModal <SourcesModal
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged.bind(this)} onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.sources} isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')} onOpenToggle={this.toggleModal.bind(this, 'sources')}
/> />
<SurveyModal
isOpen={this.state.isOpen.survey}
onOpenToggle={this.toggleModal.bind(this, 'survey')}
/>
</div> </div>
return <AppLayout return <AppLayout

View file

@ -1,22 +1,15 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import FileReaderInput from 'react-file-reader-input'
import classnames from 'classnames' import classnames from 'classnames'
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton' import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
import MdFileDownload from 'react-icons/lib/md/file-download' import MdFileDownload from 'react-icons/lib/md/file-download'
import MdFileUpload from 'react-icons/lib/md/file-upload'
import OpenIcon from 'react-icons/lib/md/open-in-browser' import OpenIcon from 'react-icons/lib/md/open-in-browser'
import SettingsIcon from 'react-icons/lib/md/settings' import SettingsIcon from 'react-icons/lib/md/settings'
import MdInfo from 'react-icons/lib/md/info'
import SourcesIcon from 'react-icons/lib/md/layers' import SourcesIcon from 'react-icons/lib/md/layers'
import MdSave from 'react-icons/lib/md/save'
import MdStyle from 'react-icons/lib/md/style'
import MdMap from 'react-icons/lib/md/map'
import MdInsertEmoticon from 'react-icons/lib/md/insert-emoticon'
import MdFontDownload from 'react-icons/lib/md/font-download'
import HelpIcon from 'react-icons/lib/md/help-outline' import HelpIcon from 'react-icons/lib/md/help-outline'
import InspectionIcon from 'react-icons/lib/md/find-in-page' import InspectionIcon from 'react-icons/lib/md/find-in-page'
import SurveyIcon from 'react-icons/lib/md/assignment-turned-in'
import ColorIcon from 'react-icons/lib/md/color-lens' import ColorIcon from 'react-icons/lib/md/color-lens'
import MapIcon from 'react-icons/lib/md/map' import MapIcon from 'react-icons/lib/md/map'
@ -26,7 +19,6 @@ import ViewIcon from 'react-icons/lib/md/remove-red-eye'
import logoImage from 'maputnik-design/logos/logo-color.svg' import logoImage from 'maputnik-design/logos/logo-color.svg'
import pkgJson from '../../package.json' import pkgJson from '../../package.json'
import style from '../libs/style'
class IconText extends React.Component { class IconText extends React.Component {
static propTypes = { static propTypes = {
@ -58,6 +50,28 @@ class ToolbarLink extends React.Component {
} }
} }
class ToolbarLinkHighlighted extends React.Component {
static propTypes = {
className: PropTypes.string,
children: PropTypes.node,
href: PropTypes.string,
onToggleModal: PropTypes.func
}
render() {
return <a
className={classnames('maputnik-toolbar-link', "maputnik-toolbar-link--highlighted", this.props.className)}
href={this.props.href}
rel="noopener noreferrer"
target="_blank"
>
<span className="maputnik-toolbar-link-wrapper">
{this.props.children}
</span>
</a>
}
}
class ToolbarAction extends React.Component { class ToolbarAction extends React.Component {
static propTypes = { static propTypes = {
children: PropTypes.node, children: PropTypes.node,
@ -89,9 +103,7 @@ export default class Toolbar extends React.Component {
onToggleModal: PropTypes.func, onToggleModal: PropTypes.func,
} }
constructor(props) { state = {
super(props)
this.state = {
isOpen: { isOpen: {
settings: false, settings: false,
sources: false, sources: false,
@ -100,7 +112,6 @@ export default class Toolbar extends React.Component {
export: false, export: false,
} }
} }
}
handleSelection(val) { handleSelection(val) {
this.props.onSetMapState(val); this.props.onSetMapState(val);
@ -211,6 +222,10 @@ export default class Toolbar extends React.Component {
<HelpIcon /> <HelpIcon />
<IconText>Help</IconText> <IconText>Help</IconText>
</ToolbarLink> </ToolbarLink>
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
<SurveyIcon />
<IconText>Take the Maputnik Survey</IconText>
</ToolbarLinkHighlighted>
</div> </div>
</div> </div>
</div> </div>

View file

@ -19,17 +19,14 @@ class ColorField extends React.Component {
default: PropTypes.string, default: PropTypes.string,
} }
constructor(props) { state = {
super(props) pickerOpened: false
this.state = {
pickerOpened: false,
}
} }
//TODO: I much rather would do this with absolute positioning //TODO: I much rather would do this with absolute positioning
//but I am too stupid to get it to work together with fixed position //but I am too stupid to get it to work together with fixed position
//and scrollbars so I have to fallback to JavaScript //and scrollbars so I have to fallback to JavaScript
calcPickerOffset() { calcPickerOffset = () => {
const elem = this.colorInput const elem = this.colorInput
if(elem) { if(elem) {
const pos = elem.getBoundingClientRect() const pos = elem.getBoundingClientRect()
@ -45,7 +42,7 @@ class ColorField extends React.Component {
} }
} }
togglePicker() { togglePicker = () => {
this.setState({ pickerOpened: !this.state.pickerOpened }) this.setState({ pickerOpened: !this.state.pickerOpened })
} }
@ -85,7 +82,7 @@ class ColorField extends React.Component {
/> />
<div <div
className="maputnik-color-picker-offset" className="maputnik-color-picker-offset"
onClick={this.togglePicker.bind(this)} onClick={this.togglePicker}
style={{ style={{
zIndex: -1, zIndex: -1,
position: 'fixed', position: 'fixed',
@ -108,7 +105,7 @@ class ColorField extends React.Component {
spellCheck="false" spellCheck="false"
className="maputnik-color" className="maputnik-color"
ref={(input) => this.colorInput = input} ref={(input) => this.colorInput = input}
onClick={this.togglePicker.bind(this)} onClick={this.togglePicker}
style={this.props.style} style={this.props.style}
name={this.props.name} name={this.props.name}
placeholder={this.props.default} placeholder={this.props.default}

View file

@ -17,7 +17,7 @@ export default class DocLabel extends React.Component {
<div className="maputnik-doc-popup"> <div className="maputnik-doc-popup">
{this.props.doc} {this.props.doc}
</div> </div>
</div > </div>
</label> </label>
} }
} }

View file

@ -32,7 +32,7 @@ export default class FunctionSpecProperty extends React.Component {
]), ]),
} }
addStop() { addStop = () => {
const stops = this.props.value.stops.slice(0) const stops = this.props.value.stops.slice(0)
const lastStop = stops[stops.length - 1] const lastStop = stops[stops.length - 1]
if (typeof lastStop[0] === "object") { if (typeof lastStop[0] === "object") {
@ -53,7 +53,7 @@ export default class FunctionSpecProperty extends React.Component {
this.props.onChange(this.props.fieldName, changedValue) this.props.onChange(this.props.fieldName, changedValue)
} }
deleteStop(stopIdx) { deleteStop = (stopIdx) => {
const stops = this.props.value.stops.slice(0) const stops = this.props.value.stops.slice(0)
stops.splice(stopIdx, 1) stops.splice(stopIdx, 1)
@ -69,7 +69,7 @@ export default class FunctionSpecProperty extends React.Component {
this.props.onChange(this.props.fieldName, changedValue) this.props.onChange(this.props.fieldName, changedValue)
} }
makeZoomFunction() { makeZoomFunction = () => {
const zoomFunc = { const zoomFunc = {
stops: [ stops: [
[6, this.props.value], [6, this.props.value],
@ -79,7 +79,7 @@ export default class FunctionSpecProperty extends React.Component {
this.props.onChange(this.props.fieldName, zoomFunc) this.props.onChange(this.props.fieldName, zoomFunc)
} }
makeDataFunction() { makeDataFunction = () => {
const dataFunc = { const dataFunc = {
property: "", property: "",
type: "categorical", type: "categorical",
@ -102,8 +102,8 @@ export default class FunctionSpecProperty extends React.Component {
fieldName={this.props.fieldName} fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
value={this.props.value} value={this.props.value}
onDeleteStop={this.deleteStop.bind(this)} onDeleteStop={this.deleteStop}
onAddStop={this.addStop.bind(this)} onAddStop={this.addStop}
/> />
) )
} }
@ -114,8 +114,8 @@ export default class FunctionSpecProperty extends React.Component {
fieldName={this.props.fieldName} fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
value={this.props.value} value={this.props.value}
onDeleteStop={this.deleteStop.bind(this)} onDeleteStop={this.deleteStop}
onAddStop={this.addStop.bind(this)} onAddStop={this.addStop}
/> />
) )
} }
@ -126,8 +126,8 @@ export default class FunctionSpecProperty extends React.Component {
fieldName={this.props.fieldName} fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
value={this.props.value} value={this.props.value}
onZoomClick={this.makeZoomFunction.bind(this)} onZoomClick={this.makeZoomFunction}
onDataClick={this.makeDataFunction.bind(this)} onDataClick={this.makeDataFunction}
/> />
) )
} }

View file

@ -42,7 +42,7 @@ export default class PropertyGroup extends React.Component {
spec: PropTypes.object.isRequired, spec: PropTypes.object.isRequired,
} }
onPropertyChange(property, newValue) { onPropertyChange = (property, newValue) => {
const group = getGroupName(this.props.spec, this.props.layer.type, property) const group = getGroupName(this.props.spec, this.props.layer.type, property)
this.props.onChange(group , property, newValue) this.props.onChange(group , property, newValue)
} }
@ -56,7 +56,7 @@ export default class PropertyGroup extends React.Component {
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName] const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
return <FunctionSpecField return <FunctionSpecField
onChange={this.onPropertyChange.bind(this)} onChange={this.onPropertyChange}
key={fieldName} key={fieldName}
fieldName={fieldName} fieldName={fieldName}
value={fieldValue === undefined ? fieldSpec.default : fieldValue} value={fieldValue === undefined ? fieldSpec.default : fieldValue}

View file

@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import color from 'color'
import ColorField from './ColorField' import ColorField from './ColorField'
import NumberInput from '../inputs/NumberInput' import NumberInput from '../inputs/NumberInput'

View file

@ -16,7 +16,7 @@ export default class FunctionButtons extends React.Component {
render() { render() {
let makeZoomButton, makeDataButton let makeZoomButton, makeDataButton
if (this.props.fieldSpec['zoom-function']) { if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
makeZoomButton = <Button makeZoomButton = <Button
className="maputnik-make-zoom-function" className="maputnik-make-zoom-function"
onClick={this.props.onZoomClick} onClick={this.props.onZoomClick}

View file

@ -13,6 +13,30 @@ import docUid from '../../libs/document-uid'
import sortNumerically from '../../libs/sort-numerically' import sortNumerically from '../../libs/sort-numerically'
/**
* We cache a reference for each stop by its index.
*
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
*/
function setStopRefs(props, state) {
// This is initialsed below only if required to improved performance.
let newRefs;
if(props.value && props.value.stops) {
props.value.stops.forEach((val, idx) => {
if(!state.refs.hasOwnProperty(idx)) {
if(!newRefs) {
newRefs = {...state};
}
newRefs[idx] = docUid("stop-");
}
})
}
return newRefs;
}
export default class ZoomProperty extends React.Component { export default class ZoomProperty extends React.Component {
static propTypes = { static propTypes = {
onChange: PropTypes.func, onChange: PropTypes.func,
@ -29,45 +53,13 @@ export default class ZoomProperty extends React.Component {
]), ]),
} }
state = {
constructor() {
super()
this.state = {
refs: {} refs: {}
} }
}
componentDidMount() { componentDidMount() {
this.setState({ const newRefs = setStopRefs(this.props, this.state);
refs: this.setStopRefs(this.props)
})
}
/**
* We cache a reference for each stop by its index.
*
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
*/
setStopRefs(props) {
// This is initialsed below only if required to improved performance.
let newRefs;
if(props.value && props.value.stops) {
props.value.stops.forEach((val, idx) => {
if(!this.state.refs.hasOwnProperty(idx)) {
if(!newRefs) {
newRefs = {...this.state.refs};
}
newRefs[idx] = docUid("stop-");
}
})
}
return newRefs;
}
UNSAFE_componentWillReceiveProps(nextProps) {
const newRefs = this.setStopRefs(nextProps);
if(newRefs) { if(newRefs) {
this.setState({ this.setState({
refs: newRefs refs: newRefs
@ -75,6 +67,16 @@ export default class ZoomProperty extends React.Component {
} }
} }
static getDerivedStateFromProps(props, state) {
const newRefs = setStopRefs(props, state);
if(newRefs) {
return {
refs: newRefs
};
}
return null;
}
// Order the stops altering the refs to reflect their new position. // Order the stops altering the refs to reflect their new position.
orderStopsByZoom(stops) { orderStopsByZoom(stops) {
const mappedWithRef = stops const mappedWithRef = stops

View file

@ -9,9 +9,6 @@ import SingleFilterEditor from './SingleFilterEditor'
import FilterEditorBlock from './FilterEditorBlock' import FilterEditorBlock from './FilterEditorBlock'
import Button from '../Button' import Button from '../Button'
import DeleteIcon from 'react-icons/lib/md/delete'
import AddIcon from 'react-icons/lib/fa/plus'
function hasCombiningFilter(filter) { function hasCombiningFilter(filter) {
return combiningFilterOps.indexOf(filter[0]) >= 0 return combiningFilterOps.indexOf(filter[0]) >= 0
} }
@ -60,7 +57,7 @@ export default class CombiningFilterEditor extends React.Component {
this.props.onChange(newFilter) this.props.onChange(newFilter)
} }
addFilterItem() { addFilterItem = () => {
const newFilterItem = this.combiningFilter().slice(0) const newFilterItem = this.combiningFilter().slice(0)
newFilterItem.push(['==', 'name', '']) newFilterItem.push(['==', 'name', ''])
this.props.onChange(newFilterItem) this.props.onChange(newFilterItem)
@ -105,7 +102,7 @@ export default class CombiningFilterEditor extends React.Component {
<Button <Button
data-wd-key="layer-filter-button" data-wd-key="layer-filter-button"
className="maputnik-add-filter" className="maputnik-add-filter"
onClick={this.addFilterItem.bind(this)}> onClick={this.addFilterItem}>
Add filter Add filter
</Button> </Button>
</div> </div>

View file

@ -14,18 +14,15 @@ class AutocompleteInput extends React.Component {
keepMenuWithinWindowBounds: PropTypes.bool keepMenuWithinWindowBounds: PropTypes.bool
} }
state = {
maxHeight: MAX_HEIGHT
}
static defaultProps = { static defaultProps = {
onChange: () => {}, onChange: () => {},
options: [], options: [],
} }
constructor(props) {
super(props);
this.state = {
maxHeight: MAX_HEIGHT
};
}
calcMaxHeight() { calcMaxHeight() {
if(this.props.keepMenuWithinWindowBounds) { if(this.props.keepMenuWithinWindowBounds) {
const maxHeight = window.innerHeight - this.autocompleteMenuEl.getBoundingClientRect().top; const maxHeight = window.innerHeight - this.autocompleteMenuEl.getBoundingClientRect().top;
@ -38,6 +35,7 @@ class AutocompleteInput extends React.Component {
} }
} }
} }
componentDidMount() { componentDidMount() {
this.calcMaxHeight(); this.calcMaxHeight();
} }

View file

@ -27,7 +27,7 @@ class DynamicArrayInput extends React.Component {
return this.props.value || this.props.default || [] return this.props.value || this.props.default || []
} }
addValue() { addValue = () => {
const values = this.values.slice(0) const values = this.values.slice(0)
if (this.props.type === 'number') { if (this.props.type === 'number') {
values.push(0) values.push(0)
@ -35,7 +35,6 @@ class DynamicArrayInput extends React.Component {
values.push("") values.push("")
} }
this.props.onChange(values) this.props.onChange(values)
} }
@ -77,7 +76,7 @@ class DynamicArrayInput extends React.Component {
{inputs} {inputs}
<Button <Button
className="maputnik-array-add-value" className="maputnik-array-add-value"
onClick={this.addValue.bind(this)} onClick={this.addValue}
> >
Add value Add value
</Button> </Button>

View file

@ -17,8 +17,10 @@ class NumberInput extends React.Component {
} }
} }
UNSAFE_componentWillReceiveProps(nextProps) { static getDerivedStateFromProps(props, state) {
this.setState({ value: nextProps.value }) return {
value: props.value
};
} }
changeValue(newValue) { changeValue(newValue) {
@ -49,7 +51,7 @@ class NumberInput extends React.Component {
return true return true
} }
resetValue() { resetValue = () => {
// Reset explicitly to default value if value has been cleared // Reset explicitly to default value if value has been cleared
if(this.state.value === "") { if(this.state.value === "") {
return this.changeValue(this.props.default) return this.changeValue(this.props.default)
@ -72,7 +74,7 @@ class NumberInput extends React.Component {
placeholder={this.props.default} placeholder={this.props.default}
value={this.state.value} value={this.state.value}
onChange={e => this.changeValue(e.target.value)} onChange={e => this.changeValue(e.target.value)}
onBlur={this.resetValue.bind(this)} onBlur={this.resetValue}
/> />
} }
} }

View file

@ -18,8 +18,10 @@ class StringInput extends React.Component {
} }
} }
UNSAFE_componentWillReceiveProps(nextProps) { static getDerivedStateFromProps(props, state) {
this.setState({ value: nextProps.value || '' }) return {
value: props.value || ''
};
} }
render() { render() {

View file

@ -29,10 +29,10 @@ class JSONEditor extends React.Component {
} }
} }
UNSAFE_componentWillReceiveProps(nextProps) { static getDerivedStateFromProps(props, state) {
this.setState({ return {
code: JSON.stringify(nextProps.layer, null, 2) code: JSON.stringify(props.layer, null, 2)
}) };
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {

View file

@ -16,9 +16,6 @@ import LayerSourceLayerBlock from './LayerSourceLayerBlock'
import MoreVertIcon from 'react-icons/lib/md/more-vert' import MoreVertIcon from 'react-icons/lib/md/more-vert'
import InputBlock from '../inputs/InputBlock'
import MultiButtonInput from '../inputs/MultiButtonInput'
import { changeType, changeProperty } from '../../libs/layer' import { changeType, changeProperty } from '../../libs/layer'
import layout from '../../config/layout.json' import layout from '../../config/layout.json'
@ -79,18 +76,18 @@ export default class LayerEditor extends React.Component {
this.state = { editorGroups } this.state = { editorGroups }
} }
UNSAFE_componentWillReceiveProps(nextProps) { static getDerivedStateFromProps(props, state) {
const additionalGroups = { ...this.state.editorGroups } const additionalGroups = { ...state.editorGroups }
layout[nextProps.layer.type].groups.forEach(group => { layout[props.layer.type].groups.forEach(group => {
if(!(group.title in additionalGroups)) { if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true additionalGroups[group.title] = true
} }
}) })
this.setState({ return {
editorGroups: additionalGroups editorGroups: additionalGroups
}) };
} }
getChildContext () { getChildContext () {

View file

@ -2,13 +2,10 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
import Button from '../Button'
import LayerListGroup from './LayerListGroup' import LayerListGroup from './LayerListGroup'
import LayerListItem from './LayerListItem' import LayerListItem from './LayerListItem'
import AddIcon from 'react-icons/lib/md/add-circle-outline'
import AddModal from '../modals/AddModal' import AddModal from '../modals/AddModal'
import style from '../../libs/style.js'
import {SortableContainer, SortableHandle} from 'react-sortable-hoc'; import {SortableContainer, SortableHandle} from 'react-sortable-hoc';
const layerListPropTypes = { const layerListPropTypes = {
@ -45,16 +42,13 @@ class LayerListContainer extends React.Component {
onLayerSelect: () => {}, onLayerSelect: () => {},
} }
constructor(props) { state = {
super(props)
this.state = {
collapsedGroups: {}, collapsedGroups: {},
areAllGroupsExpanded: false, areAllGroupsExpanded: false,
isOpen: { isOpen: {
add: false, add: false,
} }
} }
}
toggleModal(modalName) { toggleModal(modalName) {
this.setState({ this.setState({
@ -65,7 +59,7 @@ class LayerListContainer extends React.Component {
}) })
} }
toggleLayers() { toggleLayers = () => {
let idx=0 let idx=0
let newGroups=[] let newGroups=[]
@ -179,7 +173,7 @@ class LayerListContainer extends React.Component {
<div className="maputnik-multibutton"> <div className="maputnik-multibutton">
<button <button
id="skip-menu" id="skip-menu"
onClick={this.toggleLayers.bind(this)} onClick={this.toggleLayers}
className="maputnik-button"> className="maputnik-button">
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"} {this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
</button> </button>

View file

@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Color from 'color'
import classnames from 'classnames' import classnames from 'classnames'
import CopyIcon from 'react-icons/lib/md/content-copy' import CopyIcon from 'react-icons/lib/md/content-copy'
@ -9,7 +8,6 @@ import VisibilityOffIcon from 'react-icons/lib/md/visibility-off'
import DeleteIcon from 'react-icons/lib/md/delete' import DeleteIcon from 'react-icons/lib/md/delete'
import LayerIcon from '../icons/LayerIcon' import LayerIcon from '../icons/LayerIcon'
import LayerEditor from './LayerEditor'
import {SortableElement, SortableHandle} from 'react-sortable-hoc' import {SortableElement, SortableHandle} from 'react-sortable-hoc'
@SortableHandle @SortableHandle
@ -116,7 +114,7 @@ class LayerListItem extends React.Component {
/> />
<IconAction <IconAction
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"} wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
action={this.props.visibility === 'visible' ? 'hide' : 'show'} action={this.props.visibility === 'visible' ? 'show' : 'hide'}
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)} onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)}
/> />
</li> </li>

View file

@ -3,7 +3,6 @@ import PropTypes from 'prop-types'
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import AutocompleteInput from '../inputs/AutocompleteInput' import AutocompleteInput from '../inputs/AutocompleteInput'
class LayerSourceBlock extends React.Component { class LayerSourceBlock extends React.Component {

View file

@ -3,7 +3,6 @@ import PropTypes from 'prop-types'
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import AutocompleteInput from '../inputs/AutocompleteInput' import AutocompleteInput from '../inputs/AutocompleteInput'
class LayerSourceLayer extends React.Component { class LayerSourceLayer extends React.Component {

View file

@ -1,7 +1,5 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import LayerIcon from '../icons/LayerIcon' import LayerIcon from '../icons/LayerIcon'
function groupFeaturesBySourceLayer(features) { function groupFeaturesBySourceLayer(features) {

View file

@ -5,7 +5,6 @@ import MapboxGl from 'mapbox-gl'
import MapboxInspect from 'mapbox-gl-inspect' import MapboxInspect from 'mapbox-gl-inspect'
import FeatureLayerPopup from './FeatureLayerPopup' import FeatureLayerPopup from './FeatureLayerPopup'
import FeaturePropertyPopup from './FeaturePropertyPopup' import FeaturePropertyPopup from './FeaturePropertyPopup'
import style from '../../libs/style.js'
import tokens from '../../config/tokens.json' import tokens from '../../config/tokens.json'
import colors from 'mapbox-gl-inspect/lib/colors' import colors from 'mapbox-gl-inspect/lib/colors'
import Color from 'color' import Color from 'color'
@ -15,6 +14,9 @@ import 'mapbox-gl/dist/mapbox-gl.css'
import '../../mapboxgl.css' import '../../mapboxgl.css'
import '../../libs/mapbox-rtl' import '../../libs/mapbox-rtl'
const IS_SUPPORTED = MapboxGl.supported();
function renderPropertyPopup(features) { function renderPropertyPopup(features) {
var mountNode = document.createElement('div'); var mountNode = document.createElement('div');
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode) ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
@ -66,6 +68,7 @@ export default class MapboxGlMap extends React.Component {
onDataChange: () => {}, onDataChange: () => {},
onLayerSelect: () => {}, onLayerSelect: () => {},
mapboxAccessToken: tokens.mapbox, mapboxAccessToken: tokens.mapbox,
options: {},
} }
constructor(props) { constructor(props) {
@ -80,21 +83,27 @@ export default class MapboxGlMap extends React.Component {
} }
} }
UNSAFE_componentWillReceiveProps(nextProps) { updateMapFromProps(props) {
if(!IS_SUPPORTED) return;
if(!this.state.map) return if(!this.state.map) return
const metadata = nextProps.mapStyle.metadata || {} const metadata = props.mapStyle.metadata || {}
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
if(!nextProps.inspectModeEnabled) { if(!props.inspectModeEnabled) {
//Mapbox GL now does diffing natively so we don't need to calculate //Mapbox GL now does diffing natively so we don't need to calculate
//the necessary operations ourselves! //the necessary operations ourselves!
this.state.map.setStyle(nextProps.mapStyle, { diff: true}) this.state.map.setStyle(props.mapStyle, { diff: true})
} }
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if(!IS_SUPPORTED) return;
const map = this.state.map; const map = this.state.map;
this.updateMapFromProps(this.props);
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) { if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
this.state.inspect.toggleInspector() this.state.inspect.toggleInspector()
} }
@ -102,10 +111,15 @@ export default class MapboxGlMap extends React.Component {
this.state.inspect.render() this.state.inspect.render()
} }
if (map) {
map.showTileBoundaries = this.props.options.showTileBoundaries; map.showTileBoundaries = this.props.options.showTileBoundaries;
map.showCollisionBoxes = this.props.options.showCollisionBoxes;
}
} }
componentDidMount() { componentDidMount() {
if(!IS_SUPPORTED) return;
const mapOpts = { const mapOpts = {
...this.props.options, ...this.props.options,
container: this.container, container: this.container,
@ -116,6 +130,7 @@ export default class MapboxGlMap extends React.Component {
const map = new MapboxGl.Map(mapOpts); const map = new MapboxGl.Map(mapOpts);
map.showTileBoundaries = mapOpts.showTileBoundaries; map.showTileBoundaries = mapOpts.showTileBoundaries;
map.showCollisionBoxes = mapOpts.showCollisionBoxes;
const zoom = new ZoomControl; const zoom = new ZoomControl;
map.addControl(zoom, 'top-right'); map.addControl(zoom, 'top-right');
@ -161,9 +176,20 @@ export default class MapboxGlMap extends React.Component {
} }
render() { render() {
if(IS_SUPPORTED) {
return <div return <div
className="maputnik-map" className="maputnik-map__map"
ref={x => this.container = x} ref={x => this.container = x}
></div> ></div>
} }
else {
return <div
className="maputnik-map maputnik-map--error"
>
<div className="maputnik-map__error-message">
Error: Cannot load MapboxGL, WebGL is either unsupported or disabled
</div>
</div>
}
}
} }

View file

@ -1,7 +1,5 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import style from '../../libs/style.js'
import isEqual from 'lodash.isequal'
import { loadJSON } from '../../libs/urlopen' import { loadJSON } from '../../libs/urlopen'
import 'ol/ol.css' import 'ol/ol.css'
@ -29,10 +27,10 @@ class OpenLayers3Map extends React.Component {
const styleFunc = olms.apply(this.map, newMapStyle) const styleFunc = olms.apply(this.map, newMapStyle)
} }
UNSAFE_componentWillReceiveProps(nextProps) { componentDidUpdate() {
require.ensure(["ol", "ol-mapbox-style"], () => { require.ensure(["ol", "ol-mapbox-style"], () => {
if(!this.map) return if(!this.map) return
this.updateStyle(nextProps.mapStyle) this.updateStyle(this.props.mapStyle)
}) })
} }

View file

@ -2,8 +2,6 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Button from '../Button' import Button from '../Button'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import Modal from './Modal' import Modal from './Modal'
import LayerTypeBlock from '../layers/LayerTypeBlock' import LayerTypeBlock from '../layers/LayerTypeBlock'
@ -22,7 +20,7 @@ class AddModal extends React.Component {
sources: PropTypes.object.isRequired, sources: PropTypes.object.isRequired,
} }
addLayer() { addLayer = () => {
const changedLayers = this.props.layers.slice(0) const changedLayers = this.props.layers.slice(0)
const layer = { const layer = {
id: this.state.id, id: this.state.id,
@ -151,7 +149,7 @@ class AddModal extends React.Component {
} }
<Button <Button
className="maputnik-add-layer-button" className="maputnik-add-layer-button"
onClick={this.addLayer.bind(this)} onClick={this.addLayer}
data-wd-key="add-layer" data-wd-key="add-layer"
> >
Add Layer Add Layer

View file

@ -10,207 +10,9 @@ import Button from '../Button'
import Modal from './Modal' import Modal from './Modal'
import MdFileDownload from 'react-icons/lib/md/file-download' import MdFileDownload from 'react-icons/lib/md/file-download'
import TiClipboard from 'react-icons/lib/ti/clipboard' import TiClipboard from 'react-icons/lib/ti/clipboard'
import style from '../../libs/style.js' import style from '../../libs/style'
import GitHub from 'github-api'
import { CopyToClipboard } from 'react-copy-to-clipboard'
class Gist extends React.Component {
static propTypes = {
mapStyle: PropTypes.object.isRequired,
onStyleChanged: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
preview: false,
public: false,
saving: false,
latestGist: null,
}
}
UNSAFE_componentWillReceiveProps(nextProps) {
this.setState({
...this.state,
preview: !!(nextProps.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']
})
}
onSave() {
this.setState({
...this.state,
saving: true
});
const preview = this.state.preview;
const mapboxToken = (this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token'];
const mapStyleStr = preview ?
styleSpec.format(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) :
styleSpec.format(stripAccessTokens(this.props.mapStyle));
const styleTitle = this.props.mapStyle.name || 'Style';
const htmlStr = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>`+styleTitle+` Preview</title>
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.44.0/mapbox-gl.css" />
<script src="https://api.mapbox.com/mapbox-gl-js/v0.44.0/mapbox-gl.js"></script>
<style>
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
</style>
</head>
<body>
<div id='map'></div>
<script>
mapboxgl.accessToken = '${mapboxToken}';
var map = new mapboxgl.Map({
container: 'map',
style: 'style.json',
attributionControl: true,
hash: true
});
map.addControl(new mapboxgl.NavigationControl());
</script>
</body>
</html>
`
const files = {
"style.json": {
content: mapStyleStr
}
}
if(preview) {
files["index.html"] = {
content: htmlStr
}
}
const gh = new GitHub();
let gist = gh.getGist(); // not a gist yet
gist.create({
public: this.state.public,
description: styleTitle,
files: files
}).then(function({data}) {
return gist.read();
}).then(function({data}) {
this.setState({
...this.state,
latestGist: data,
saving: false,
});
}.bind(this));
}
onPreviewChange(value) {
this.setState({
...this.state,
preview: value
})
}
onPublicChange(value) {
this.setState({
...this.state,
public: value
})
}
changeMetadataProperty(property, value) {
const changedStyle = {
...this.props.mapStyle,
metadata: {
...this.props.mapStyle.metadata,
[property]: value
}
}
this.props.onStyleChanged(changedStyle)
}
renderPreviewLink() {
const gist = this.state.latestGist;
const user = gist.user || 'anonymous';
const preview = !!gist.files['index.html'];
if(preview) {
return <span><a target="_blank" rel="noopener noreferrer" href={"https://bl.ocks.org/"+user+"/"+gist.id}>Preview</a>,{' '}</span>
}
return null;
}
renderLatestGist() {
const gist = this.state.latestGist;
const saving = this.state.saving;
if(saving) {
return <p>Saving...</p>
} else if(gist) {
const user = gist.user || 'anonymous';
const rawGistLink = "https://gist.githubusercontent.com/" + user + "/" + gist.id + "/raw/" + gist.history[0].version + "/style.json"
const maputnikStyleLink = "https://maputnik.github.io/editor/?style=" + rawGistLink
return <div className="maputnik-render-gist">
<p>
Latest saved gist:{' '}
{this.renderPreviewLink(this)}
<a target="_blank" rel="noopener noreferrer" href={"https://gist.github.com/" + user + "/" + gist.id}>Source</a>
</p>
<p>
<CopyToClipboard text={maputnikStyleLink}>
<span>Share this style: <Button><TiClipboard size={18} /></Button></span>
</CopyToClipboard>
<StringInput value={maputnikStyleLink} />
</p>
</div>
}
}
render() {
return <div className="maputnik-export-gist">
<Button onClick={this.onSave.bind(this)}>
<MdFileDownload />
Save to Gist (anonymous)
</Button>
<div className="maputnik-modal-sub-section">
<CheckboxInput
value={this.state.public}
name='gist-style-public'
onChange={this.onPublicChange.bind(this)}
/>
<span> Public gist</span>
</div>
<div className="maputnik-modal-sub-section">
<CheckboxInput
value={this.state.preview}
name='gist-style-preview'
onChange={this.onPreviewChange.bind(this)}
/>
<span> Include preview</span>
</div>
{this.state.preview ?
<div>
<InputBlock
label={"OpenMapTiles Access Token: "}>
<StringInput
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}/>
</InputBlock>
<InputBlock
label={"Mapbox Access Token: "}>
<StringInput
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}/>
</InputBlock>
<a target="_blank" rel="noopener noreferrer" href="https://openmaptiles.com/hosting/">Get your free access token</a>
</div>
: null}
{this.renderLatestGist()}
</div>
}
}
function stripAccessTokens(mapStyle) { function stripAccessTokens(mapStyle) {
const changedMetadata = { ...mapStyle.metadata } const changedMetadata = { ...mapStyle.metadata }
@ -235,10 +37,24 @@ class ExportModal extends React.Component {
} }
downloadStyle() { downloadStyle() {
const blob = new Blob([styleSpec.format(stripAccessTokens(this.props.mapStyle))], {type: "application/json;charset=utf-8"}); const tokenStyle = styleSpec.format(stripAccessTokens(style.replaceAccessTokens(this.props.mapStyle)));
const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
saveAs(blob, this.props.mapStyle.id + ".json"); saveAs(blob, this.props.mapStyle.id + ".json");
} }
changeMetadataProperty(property, value) {
const changedStyle = {
...this.props.mapStyle,
metadata: {
...this.props.mapStyle.metadata,
[property]: value
}
}
this.props.onStyleChanged(changedStyle)
}
render() { render() {
return <Modal return <Modal
data-wd-key="export-modal" data-wd-key="export-modal"
@ -252,16 +68,34 @@ class ExportModal extends React.Component {
<p> <p>
Download a JSON style to your computer. Download a JSON style to your computer.
</p> </p>
<p>
<InputBlock label={"MapTiler Access Token: "}>
<StringInput
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/>
</InputBlock>
<InputBlock label={"Mapbox Access Token: "}>
<StringInput
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
/>
</InputBlock>
<InputBlock label={"Thunderforest Access Token: "}>
<StringInput
value={(this.props.mapStyle.metadata || {})['maputnik:thunderforest_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/>
</InputBlock>
</p>
<Button onClick={this.downloadStyle.bind(this)}> <Button onClick={this.downloadStyle.bind(this)}>
<MdFileDownload /> <MdFileDownload />
Download Download
</Button> </Button>
</div> </div>
<div className="maputnik-modal-section hide">
<h4>Save style</h4>
<Gist mapStyle={this.props.mapStyle} onStyleChanged={this.props.onStyleChanged}/>
</div>
</Modal> </Modal>
} }
} }

View file

@ -13,10 +13,6 @@ class LoadingModal extends React.Component {
message: PropTypes.node.isRequired, message: PropTypes.node.isRequired,
} }
constructor(props) {
super(props);
}
underlayOnClick(e) { underlayOnClick(e) {
// This stops click events falling through to underlying modals. // This stops click events falling through to underlying modals.
e.stopPropagation(); e.stopPropagation();

View file

@ -4,7 +4,6 @@ import LoadingModal from './LoadingModal'
import Modal from './Modal' import Modal from './Modal'
import Button from '../Button' import Button from '../Button'
import FileReaderInput from 'react-file-reader-input' import FileReaderInput from 'react-file-reader-input'
import request from 'request'
import FileUploadIcon from 'react-icons/lib/md/file-upload' import FileUploadIcon from 'react-icons/lib/md/file-upload'
import AddIcon from 'react-icons/lib/md/add-circle-outline' import AddIcon from 'react-icons/lib/md/add-circle-outline'
@ -74,42 +73,48 @@ class OpenModal extends React.Component {
} }
} }
onStyleSelect(styleUrl) { onStyleSelect = (styleUrl) => {
this.clearError(); this.clearError();
const reqOpts = { const activeRequest = fetch(styleUrl, {
url: styleUrl, mode: 'cors',
withCredentials: false, credentials: "same-origin"
} })
.then(function(response) {
const activeRequest = request(reqOpts, (error, response, body) => { return response.json();
})
.then((body) => {
this.setState({ this.setState({
activeRequest: null, activeRequest: null,
activeRequestUrl: null activeRequestUrl: null
}); });
if (!error && response.statusCode == 200) { const mapStyle = style.ensureStyleValidity(body)
const mapStyle = style.ensureStyleValidity(JSON.parse(body))
console.log('Loaded style ', mapStyle.id) console.log('Loaded style ', mapStyle.id)
this.props.onStyleOpen(mapStyle) this.props.onStyleOpen(mapStyle)
this.onOpenToggle() this.onOpenToggle()
} else { })
.catch((err) => {
this.setState({
activeRequest: null,
activeRequestUrl: null
});
console.error(err);
console.warn('Could not open the style URL', styleUrl) console.warn('Could not open the style URL', styleUrl)
}
}) })
this.setState({ this.setState({
activeRequest: activeRequest, activeRequest: activeRequest,
activeRequestUrl: reqOpts.url activeRequestUrl: styleUrl
}) })
} }
onOpenUrl() { onOpenUrl = () => {
const url = this.styleUrlElement.value; const url = this.styleUrlElement.value;
this.onStyleSelect(url); this.onStyleSelect(url);
} }
onUpload(_, files) { onUpload = (_, files) => {
const [e, file] = files[0]; const [e, file] = files[0];
const reader = new FileReader(); const reader = new FileReader();
@ -146,7 +151,7 @@ class OpenModal extends React.Component {
url={style.url} url={style.url}
title={style.title} title={style.title}
thumbnailUrl={style.thumbnail} thumbnailUrl={style.thumbnail}
onSelect={this.onStyleSelect.bind(this)} onSelect={this.onStyleSelect}
/> />
}) })
@ -170,7 +175,7 @@ class OpenModal extends React.Component {
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h2>Upload Style</h2> <h2>Upload Style</h2>
<p>Upload a JSON style from your computer.</p> <p>Upload a JSON style from your computer.</p>
<FileReaderInput onChange={this.onUpload.bind(this)} tabIndex="-1"> <FileReaderInput onChange={this.onUpload} tabIndex="-1">
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button> <Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
</FileReaderInput> </FileReaderInput>
</section> </section>
@ -182,7 +187,7 @@ class OpenModal extends React.Component {
</p> </p>
<input data-wd-key="open-modal.url.input" type="text" ref={(input) => this.styleUrlElement = input} className="maputnik-input" placeholder="Enter URL..."/> <input data-wd-key="open-modal.url.input" type="text" ref={(input) => this.styleUrlElement = input} className="maputnik-input" placeholder="Enter URL..."/>
<div> <div>
<Button data-wd-key="open-modal.url.button" className="maputnik-big-button" onClick={this.onOpenUrl.bind(this)}>Open URL</Button> <Button data-wd-key="open-modal.url.button" className="maputnik-big-button" onClick={this.onOpenUrl}>Open URL</Button>
</div> </div>
</section> </section>

View file

@ -15,10 +15,6 @@ class SettingsModal extends React.Component {
onOpenToggle: PropTypes.func.isRequired, onOpenToggle: PropTypes.func.isRequired,
} }
constructor(props) {
super(props);
}
changeStyleProperty(property, value) { changeStyleProperty(property, value) {
const changedStyle = { const changedStyle = {
...this.props.mapStyle, ...this.props.mapStyle,
@ -86,7 +82,7 @@ class SettingsModal extends React.Component {
/> />
</InputBlock> </InputBlock>
<InputBlock label={"OpenMapTiles Access Token"} doc={"Public access token for the OpenMapTiles CDN."}> <InputBlock label={"MapTiler Access Token"} doc={"Public access token for MapTiler Cloud."}>
<StringInput {...inputProps} <StringInput {...inputProps}
data-wd-key="modal-settings.maputnik:openmaptiles_access_token" data-wd-key="modal-settings.maputnik:openmaptiles_access_token"
value={metadata['maputnik:openmaptiles_access_token']} value={metadata['maputnik:openmaptiles_access_token']}
@ -94,12 +90,20 @@ class SettingsModal extends React.Component {
/> />
</InputBlock> </InputBlock>
<InputBlock label={"Thunderforest Access Token"} doc={"Public access token for Thunderforest services."}>
<StringInput {...inputProps}
data-wd-key="modal-settings.maputnik:thunderforest_access_token"
value={metadata['maputnik:thunderforest_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/>
</InputBlock>
<InputBlock label={"Style Renderer"} doc={"Choose the default Maputnik renderer for this style."}> <InputBlock label={"Style Renderer"} doc={"Choose the default Maputnik renderer for this style."}>
<SelectInput {...inputProps} <SelectInput {...inputProps}
data-wd-key="modal-settings.maputnik:renderer" data-wd-key="modal-settings.maputnik:renderer"
options={[ options={[
['mbgljs', 'MapboxGL JS'], ['mbgljs', 'MapboxGL JS'],
['ol3', 'Open Layers 3'], // ['ol3', 'Open Layers 3'],
]} ]}
value={metadata['maputnik:renderer'] || 'mbgljs'} value={metadata['maputnik:renderer'] || 'mbgljs'}
onChange={this.changeMetadataProperty.bind(this, 'maputnik:renderer')} onChange={this.changeMetadataProperty.bind(this, 'maputnik:renderer')}

View file

@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Button from '../Button'
import Modal from './Modal' import Modal from './Modal'
@ -11,10 +10,6 @@ class ShortcutsModal extends React.Component {
onOpenToggle: PropTypes.func.isRequired, onOpenToggle: PropTypes.func.isRequired,
} }
constructor(props) {
super(props);
}
render() { render() {
const help = [ const help = [
{ {

View file

@ -236,9 +236,12 @@ class SourcesModal extends React.Component {
<p> <p>
Add one of the publicly available sources to your style. Add one of the publicly available sources to your style.
</p> </p>
<div style={{maxwidth: 500}}> <div className="maputnik-public-sources" style={{maxwidth: 500}}>
{tilesetOptions} {tilesetOptions}
</div> </div>
<p>
<strong>Note:</strong> Some of the tilesets are not optimised for online use, and as a result the file sizes of the tiles can be quite large (heavy) for online vector rendering. Please review any tilesets before use.
</p>
</div> </div>
<div className="maputnik-modal-section"> <div className="maputnik-modal-section">

View file

@ -0,0 +1,39 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '../Button'
import Modal from './Modal'
import logoImage from 'maputnik-design/logos/logo-color.svg'
class SurveyModal extends React.Component {
static propTypes = {
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
}
onClick = () => {
window.open('https://gregorywolanski.typeform.com/to/cPgaSY', '_blank');
this.props.onOpenToggle();
}
render() {
return <Modal
data-wd-key="modal-survey"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title="Maputnik Survey"
>
<div className="maputnik-modal-survey">
<img className="maputnik-modal-survey__logo" src={logoImage} alt="" width="128" />
<h1>You + Maputnik = Maputnik better for you</h1>
<p className="maputnik-modal-survey__description">We dont track you, so we dont know how you use Maputnik. Help us make Maputnik better for you by completing a 7minute survey carried out by our contributing designer.</p>
<Button onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</Button>
<p className="maputnik-modal-survey__footnote">It takes 7 minutes, tops! Every question is optional.</p>
</div>
</Modal>
}
}
export default SurveyModal

View file

@ -193,7 +193,8 @@
"raster-brightness-max", "raster-brightness-max",
"raster-saturation", "raster-saturation",
"raster-contrast", "raster-contrast",
"raster-fade-duration" "raster-fade-duration",
"raster-resampling"
] ]
} }
] ]

View file

@ -26,31 +26,13 @@
{ {
"id": "osm-liberty", "id": "osm-liberty",
"title": "OSM Liberty", "title": "OSM Liberty",
"url": "https://rawgit.com/lukasmartinelli/osm-liberty/gh-pages/style.json", "url": "https://rawgit.com/maputnik/osm-liberty/gh-pages/style.json",
"thumbnail": "https://cdn.rawgit.com/lukasmartinelli/osm-liberty/gh-pages/thumbnail.png" "thumbnail": "https://cdn.rawgit.com/maputnik/osm-liberty/gh-pages/thumbnail.png"
}, },
{ {
"id": "empty-style", "id": "empty-style",
"title": "Empty Style", "title": "Empty Style",
"url": "https://rawgit.com/maputnik/editor/master/src/config/empty-style.json", "url": "https://rawgit.com/maputnik/editor/master/src/config/empty-style.json",
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII=" "thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII="
},
{
"id": "mapbox-satellite",
"title": "Mapbox Satellite",
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/satellite-v9.json",
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-satellite.png"
},
{
"id": "mapbox-bright",
"title": "Mapbox Bright",
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/bright-v9.json",
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-bright.png"
},
{
"id": "mapbox-basic",
"title": "Mapbox Basic",
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/basic-v9.json",
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-basic.png"
} }
] ]

View file

@ -1,12 +1,17 @@
{ {
"mapbox-streets": {
"type": "vector",
"url": "mapbox://mapbox.mapbox-streets-v7",
"title": "Mapbox Streets"
},
"openmaptiles": { "openmaptiles": {
"type": "vector", "type": "vector",
"url": "https://free.tilehosting.com/data/v3.json?key={key}", "url": "https://free.tilehosting.com/data/v3.json?key={key}",
"title": "OpenMapTiles" "title": "OpenMapTiles"
},
"thunderforest_transport": {
"type": "vector",
"url": "https://tile.thunderforest.com/thunderforest.transport-v1.json?apikey={key}",
"title": "Thunderforest Transport (heavy)"
},
"thunderforest_outdoors": {
"type": "vector",
"url": "https://tile.thunderforest.com/thunderforest.outdoors-v1.json?apikey={key}",
"title": "Thunderforest Outdoors (heavy)"
} }
} }

View file

@ -1,4 +1,5 @@
{ {
"mapbox": "pk.eyJ1IjoibW9yZ2Vua2FmZmVlIiwiYSI6ImNpeHJmNXNmZTAwNHIycXBid2NqdTJibjMifQ.Dv1-GDpTWi0NP6xW9Fct1w", "mapbox": "pk.eyJ1IjoibW9yZ2Vua2FmZmVlIiwiYSI6ImNpeHJmNXNmZTAwNHIycXBid2NqdTJibjMifQ.Dv1-GDpTWi0NP6xW9Fct1w",
"openmaptiles": "Og58UhhtiiTaLVlPtPgs" "openmaptiles": "Og58UhhtiiTaLVlPtPgs",
"thunderforest": "b71f7f0ba4064f5eb9e903859a9cf5c6"
} }

View file

@ -1,8 +1,8 @@
import lodash from 'lodash' import throttle from 'lodash.throttle'
// Throttle for 3 seconds so when a user enables it they don't have to refresh the page. // Throttle for 3 seconds so when a user enables it they don't have to refresh the page.
const reducedMotionEnabled = lodash.throttle(() => { const reducedMotionEnabled = throttle(() => {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches return window.matchMedia("(prefers-reduced-motion: reduce)").matches
}, 3000); }, 3000);

View file

@ -1,4 +1,3 @@
import request from 'request'
import style from './style.js' import style from './style.js'
import ReconnectingWebSocket from 'reconnecting-websocket' import ReconnectingWebSocket from 'reconnecting-websocket'
@ -14,15 +13,20 @@ export class ApiStyleStore {
} }
init(cb) { init(cb) {
request(localUrl + '/styles', (error, response, body) => { fetch(localUrl + '/styles', {
if (!error && body && response.statusCode == 200) { mode: 'cors',
const styleIds = JSON.parse(body) })
.then(function(response) {
return response.json();
})
.then(function(body) {
const styleIds = body;
this.latestStyleId = styleIds[0] this.latestStyleId = styleIds[0]
this.notifyLocalChanges() this.notifyLocalChanges()
cb(null) cb(null)
} else { })
.catch(function() {
cb(new Error('Can not connect to style API')) cb(new Error('Can not connect to style API'))
}
}) })
} }
@ -44,8 +48,14 @@ export class ApiStyleStore {
latestStyle(cb) { latestStyle(cb) {
if(this.latestStyleId) { if(this.latestStyleId) {
request(localUrl + '/styles/' + this.latestStyleId, (error, response, body) => { fetch(localUrl + '/styles/' + this.latestStyleId, {
cb(style.ensureStyleValidity(JSON.parse(body))) mode: 'cors',
})
.then(function(response) {
return response.json();
})
.then(function(body) {
cb(style.ensureStyleValidity(body))
}) })
} else { } else {
throw new Error('No latest style available. You need to init the api backend first.') throw new Error('No latest style available. You need to init the api backend first.')
@ -55,11 +65,15 @@ export class ApiStyleStore {
// Save current style replacing previous version // Save current style replacing previous version
save(mapStyle) { save(mapStyle) {
const id = mapStyle.id const id = mapStyle.id
request.put({ fetch(localUrl + '/styles/' + id, {
url: localUrl + '/styles/' + id, method: "PUT",
json: true, mode: 'cors',
body: mapStyle headers: {
}, (error, response, body) => { "Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify(mapStyle)
})
.catch(function(error) {
if(error) console.error(error) if(error) console.error(error)
}) })
return mapStyle return mapStyle

View file

@ -1,9 +1,9 @@
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js' import MapboxGl from 'mapbox-gl'
// Load mapbox-gl-rtl-text using object urls without needing http://localhost for AJAX. // Load mapbox-gl-rtl-text using object urls without needing http://localhost for AJAX.
const data = require("base64-loader?mimetype=text/javascript!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.js"); const data = require("raw-loader?mimetype=text/javascript!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.js");
const blob = new window.Blob([window.atob(data)]); const blob = new window.Blob([data]);
const objectUrl = window.URL.createObjectURL(blob, { const objectUrl = window.URL.createObjectURL(blob, {
type: "text/javascript" type: "text/javascript"
}); });

View file

@ -1,22 +1,19 @@
import request from 'request'
import npmurl from 'url' import npmurl from 'url'
function loadJSON(url, defaultValue, cb) { function loadJSON(url, defaultValue, cb) {
request({ fetch(url, {
url: url, mode: 'cors',
withCredentials: false, credentials: "same-origin"
}, (error, response, body) => { })
if (!error && body && response.statusCode == 200) { .then(function(response) {
try { return response.json();
cb(JSON.parse(body)) })
} catch(err) { .then(function(body) {
console.error(err) cb(body)
cb(defaultValue) })
} .catch(function() {
} else {
console.warn('Can not metadata for ' + url) console.warn('Can not metadata for ' + url)
cb(defaultValue) cb(defaultValue)
}
}) })
} }

View file

@ -1,4 +1,3 @@
import React from 'react';
import deref from '@mapbox/mapbox-gl-style-spec/deref' import deref from '@mapbox/mapbox-gl-style-spec/deref'
import tokens from '../config/tokens.json' import tokens from '../config/tokens.json'
@ -54,18 +53,28 @@ function indexOfLayer(layers, layerId) {
return null return null
} }
function replaceAccessToken(mapStyle, opts={}) { function getAccessToken(sourceName, mapStyle, opts) {
const omtSource = mapStyle.sources.openmaptiles if(sourceName === "thunderforest_transport" || sourceName === "thunderforest_outdoors") {
if(!omtSource) return mapStyle sourceName = "thunderforest"
if(!omtSource.hasOwnProperty("url")) return mapStyle }
const metadata = mapStyle.metadata || {} const metadata = mapStyle.metadata || {}
let accessToken = metadata['maputnik:openmaptiles_access_token']; let accessToken = metadata[`maputnik:${sourceName}_access_token`]
if(opts.allowFallback && !accessToken) { if(opts.allowFallback && !accessToken) {
accessToken = tokens.openmaptiles; accessToken = tokens[sourceName]
} }
return accessToken;
}
function replaceSourceAccessToken(mapStyle, sourceName, opts={}) {
const source = mapStyle.sources[sourceName]
if(!source) return mapStyle
if(!source.hasOwnProperty("url")) return mapStyle
const accessToken = getAccessToken(sourceName, mapStyle, opts)
if(!accessToken) { if(!accessToken) {
// Early exit. // Early exit.
return mapStyle; return mapStyle;
@ -73,16 +82,34 @@ function replaceAccessToken(mapStyle, opts={}) {
const changedSources = { const changedSources = {
...mapStyle.sources, ...mapStyle.sources,
openmaptiles: { [sourceName]: {
...omtSource, ...source,
url: omtSource.url.replace('{key}', accessToken) url: source.url.replace('{key}', accessToken)
} }
} }
const changedStyle = { const changedStyle = {
...mapStyle, ...mapStyle,
glyphs: mapStyle.glyphs ? mapStyle.glyphs.replace('{key}', accessToken) : mapStyle.glyphs,
sources: changedSources sources: changedSources
} }
return changedStyle
}
function replaceAccessTokens(mapStyle, opts={}) {
let changedStyle = mapStyle
Object.keys(mapStyle.sources).forEach((sourceName) => {
changedStyle = replaceSourceAccessToken(changedStyle, sourceName, opts);
})
if (mapStyle.glyphs && mapStyle.glyphs.match(/\.tilehosting\.com/)) {
const newAccessToken = getAccessToken("openmaptiles", mapStyle, opts);
if (newAccessToken) {
changedStyle = {
...changedStyle,
glyphs: mapStyle.glyphs.replace('{key}', newAccessToken)
}
}
}
return changedStyle return changedStyle
} }
@ -92,5 +119,5 @@ export default {
emptyStyle, emptyStyle,
indexOfLayer, indexOfLayer,
generateId, generateId,
replaceAccessToken, replaceAccessTokens,
} }

View file

@ -1,8 +1,6 @@
import { colorizeLayers } from './style.js'
import style from './style.js' import style from './style.js'
import { loadStyleUrl } from './urlopen' import { loadStyleUrl } from './urlopen'
import publicSources from '../config/styles.json' import publicSources from '../config/styles.json'
import request from 'request'
const storagePrefix = "maputnik" const storagePrefix = "maputnik"
const stylePrefix = 'style' const stylePrefix = 'style'

View file

@ -1,4 +1,3 @@
import request from 'request'
import url from 'url' import url from 'url'
import style from './style.js' import style from './style.js'
@ -9,34 +8,40 @@ export function initialStyleUrl() {
export function loadStyleUrl(styleUrl, cb) { export function loadStyleUrl(styleUrl, cb) {
console.log('Loading style', styleUrl) console.log('Loading style', styleUrl)
request({ fetch(styleUrl, {
url: styleUrl, mode: 'cors',
withCredentials: false, credentials: "same-origin"
}, (error, response, body) => { })
if (!error && response.statusCode == 200) { .then(function(response) {
cb(style.ensureStyleValidity(JSON.parse(body))) return response.json();
} else { })
.then(function(body) {
cb(style.ensureStyleValidity(body))
})
.catch(function() {
console.warn('Could not fetch default style', styleUrl) console.warn('Could not fetch default style', styleUrl)
cb(style.emptyStyle) cb(style.emptyStyle)
}
}) })
} }
export function loadJSON(url, defaultValue, cb) { export function loadJSON(url, defaultValue, cb) {
request({ fetch(url, {
url: url, mode: 'cors',
withCredentials: false, credentials: "same-origin"
}, (error, response, body) => { })
if (!error && body && response.statusCode == 200) { .then(function(response) {
return response.json();
})
.then(function(body) {
try { try {
cb(JSON.parse(body)) cb(body)
} catch(err) { } catch(err) {
console.error(err) console.error(err)
cb(defaultValue) cb(defaultValue)
} }
} else { })
.catch(function() {
console.error('Can not load JSON from ' + url) console.error('Can not load JSON from ' + url)
cb(defaultValue) cb(defaultValue)
}
}) })
} }

View file

@ -18,6 +18,11 @@ html {
box-sizing: border-box; box-sizing: border-box;
} }
body {
// The UI is 100% height so prevent bounce scroll on OSX
overflow: hidden;
}
*, *,
*::before, *::before,
*::after { *::after {

View file

@ -1,11 +1,31 @@
// MAP // MAP
.maputnik-map { .maputnik-map__container {
display: flex;
position: fixed !important; position: fixed !important;
top: $toolbar-height + $toolbar-offset; top: $toolbar-height + $toolbar-offset;
right: 0; right: 0;
bottom: 0; bottom: 0;
height: calc(100% - #{$toolbar-height + $toolbar-offset}); height: calc(100% - #{$toolbar-height + $toolbar-offset});
width: 75%; width: calc(
100%
- 200px /* layer list */
- 350px /* layer editor */
);
&--error {
align-items: center;
justify-content: center;
}
&__error-message {
margin: 16px;
text-align: center;
}
}
.maputnik-map__map {
width: 100%;
height: 100%;
} }
// DOC LABEL // DOC LABEL
@ -56,6 +76,7 @@
border-width: 0; border-width: 0;
border-radius: 2px; border-radius: 2px;
box-sizing: border-box; box-sizing: border-box;
text-decoration: none;
&:hover { &:hover {
background-color: lighten($color-midgray, 12); background-color: lighten($color-midgray, 12);
@ -70,6 +91,20 @@
font-size: $font-size-5; font-size: $font-size-5;
} }
.maputnik-wide-button {
padding: $margin-2 $margin-3;
}
.maputnik-green-button {
background-color: $color-green;
color: $color-black;
}
.maputnik-white-button {
background-color: $color-white;
color: $color-black;
}
.maputnik-icon-button { .maputnik-icon-button {
background-color: transparent; background-color: transparent;

View file

@ -1,6 +1,6 @@
//SCROLLING //SCROLLING
.maputnik-scroll-container { .maputnik-scroll-container {
overflow-x: visible; overflow-x: hidden;
overflow-y: scroll; overflow-y: scroll;
bottom: 0; bottom: 0;
left: 0; left: 0;

View file

@ -125,6 +125,10 @@
} }
//SOURCE MODAL //SOURCE MODAL
.maputnik-public-sources {
margin-bottom: 1.5%;
}
.maputnik-public-source { .maputnik-public-source {
vertical-align: top; vertical-align: top;
margin-top: 1.5%; margin-top: 1.5%;
@ -150,6 +154,7 @@
.maputnik-public-source-id { .maputnik-public-source-id {
font-weight: 400; font-weight: 400;
text-align: left;
} }
.maputnik-active-source-type-editor { .maputnik-active-source-type-editor {
@ -240,3 +245,21 @@
margin-bottom: 4px; margin-bottom: 4px;
} }
} }
.maputnik-modal-survey {
width: 372px;
}
.maputnik-modal-survey__logo {
display: block;
margin: 0 auto;
}
.maputnik-modal-survey__description {
line-height: 1.5;
}
.maputnik-modal-survey__footnote {
color: $color-green;
margin-top: 16px;
}

View file

@ -0,0 +1,3 @@
.react-codemirror2 {
max-width: 100%;
}

View file

@ -1,6 +1,7 @@
// See <https://github.com/nkbt/react-collapse/commit/4f4fbce7c6c07b082dc62062338c9294c656f9df> // See <https://github.com/nkbt/react-collapse/commit/4f4fbce7c6c07b082dc62062338c9294c656f9df>
.react-collapse-container { .react-collapse-container {
display: flex; display: flex;
max-width: 100%;
> * { > * {
flex: 1; flex: 1;

View file

@ -57,6 +57,29 @@
} }
} }
.maputnik-toolbar-link--highlighted {
line-height: 1;
padding: $margin-2 $margin-3;
.maputnik-toolbar-link-wrapper {
background-color: $color-white;
border-radius: 2px;
padding: $margin-2;
margin-top: $margin-1;
color: $color-black;
display: block;
}
&:hover {
background-color: $color-black;
}
&:hover .maputnik-toolbar-link-wrapper {
background-color: lighten($color-midgray, 12);
color: $color-white;
}
}
.maputnik-toolbar-version { .maputnik-toolbar-version {
font-size: 10px; font-size: 10px;
margin-left: 4px; margin-left: 4px;

View file

@ -4,6 +4,7 @@ $color-midgray: #36383e;
$color-lowgray: #8e8e8e; $color-lowgray: #8e8e8e;
$color-white: #f0f0f0; $color-white: #f0f0f0;
$color-red: #cf4a4a; $color-red: #cf4a4a;
$color-green: #53b972;
$margin-1: 3px; $margin-1: 3px;
$margin-2: 5px; $margin-2: 5px;
$margin-3: 10px; $margin-3: 10px;
@ -37,6 +38,7 @@ $toolbar-offset: 0;
@import 'popup'; @import 'popup';
@import 'map'; @import 'map';
@import 'react-collapse'; @import 'react-collapse';
@import 'react-codemirror';
/** /**
* Hacks for webdriverio isVisibleWithinViewport * Hacks for webdriverio isVisibleWithinViewport

View file

@ -1,7 +1,6 @@
var assert = require("assert"); var assert = require("assert");
var config = require("../../config/specs"); var config = require("../../config/specs");
var helper = require("../helper"); var helper = require("../helper");
var wd = require("../../wd-helper");
describe.skip("history", function() { describe.skip("history", function() {

View file

@ -1,6 +1,4 @@
var assert = require('assert');
var config = require("../config/specs"); var config = require("../config/specs");
var geoServer = require("../geojson-server");
var helper = require("./helper"); var helper = require("./helper");
require("./util/webdriverio-ext"); require("./util/webdriverio-ext");
@ -13,6 +11,9 @@ describe('maputnik', function() {
"geojson:example", "geojson:example",
"raster:raster" "raster:raster"
])); ]));
browser.execute(function() {
localStorage.setItem("survey", true);
});
browser.waitForExist(".maputnik-toolbar-link"); browser.waitForExist(".maputnik-toolbar-link");
browser.flushReactUpdates(); browser.flushReactUpdates();
}); });

View file

@ -1,5 +1,3 @@
var assert = require('assert');
var wd = require("../../wd-helper");
var config = require("../../config/specs"); var config = require("../../config/specs");
var helper = require("../helper"); var helper = require("../helper");

View file

@ -171,7 +171,7 @@ describe("modals", function() {
assert.equal(styleObj.metadata["maputnik:openmaptiles_access_token"], apiKey); assert.equal(styleObj.metadata["maputnik:openmaptiles_access_token"], apiKey);
}) })
it("style renderer", function() { it.skip("style renderer", function() {
var selector = wd.$("modal-settings.maputnik:renderer"); var selector = wd.$("modal-settings.maputnik:renderer");
browser.selectByValue(selector, "ol3"); browser.selectByValue(selector, "ol3");
browser.click(wd.$("modal-settings.name")) browser.click(wd.$("modal-settings.name"))

View file

@ -1,4 +1,3 @@
var artifacts = require("../../artifacts");
var config = require("../../config/specs"); var config = require("../../config/specs");
var helper = require("../helper"); var helper = require("../helper");
var wd = require("../../wd-helper"); var wd = require("../../wd-helper");

View file

@ -46,10 +46,10 @@ try {
browser.addCommand('flushReactUpdates', function() { browser.addCommand('flushReactUpdates', function() {
browser.executeAsync(function(done) { browser.executeAsync(function(done) {
// For any events to propogate // For any events to propogate
setImmediate(function() { setTimeout(function() {
// For the DOM to be updated. // For the DOM to be updated.
setImmediate(done); setTimeout(done, 0);
}) }, 0)
}) })
}) })