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,9 +5,9 @@
"test": {
"plugins": [
["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.
## Donations
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
## Documentation
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.
**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.
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:
matrix:
- nodejs_version: "6"
- nodejs_version: "8"
- nodejs_version: "9"
- nodejs_version: "10"
platform:
- x86
- x64

View file

@ -14,25 +14,6 @@ var OUTPATH = artifacts.pathSync("/build");
module.exports = {
entry: {
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: {
path: OUTPATH,
@ -55,7 +36,6 @@ module.exports = {
},
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: '[chunkhash].vendor.js' }),
new WebpackCleanupPlugin(),
new webpack.DefinePlugin({
'process.env': {

86
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "maputnik",
"version": "1.2.0",
"version": "1.5.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -281,25 +281,20 @@
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.1.tgz",
"integrity": "sha512-LGegvJq+ks4UFnEAvtAhygFRZxaNg2vw7PsvLOAXY8ziJLv5aFDSdHDEMJA/Q8PG5dNhMioUvo0kNqb7U5dPoQ=="
},
"@mapbox/mapbox-gl-rtl-text": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-rtl-text/-/mapbox-gl-rtl-text-0.1.2.tgz",
"integrity": "sha512-x5xSHNAD5MeuasbEpGyDBGy4zPQysDdvDUlnjDKMxic8KqmcxcCP3ojlIaqEWYFvMmq7FKyWIF7K8jO4bHavKQ=="
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-rtl-text/-/mapbox-gl-rtl-text-0.2.0.tgz",
"integrity": "sha512-oijFgP0DTMRtLzEpZM5lCrlagoQaOXUhZOMKOXaCF56QohKdRO4uu8RUKcuvfpWz6ruQcyH2yikBl09aWh3Duw=="
},
"@mapbox/mapbox-gl-style-spec": {
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-12.0.0.tgz",
"integrity": "sha1-dRW/rbs6hZ66HFbZn8E4P6YpJ0c=",
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.1.0.tgz",
"integrity": "sha512-WoviOAN39bZ3Fm3jBycGZg64rAihZ1ykVOR/XAiJLPviyPGTSWo0kIzcW2FRg23LJ0FSysEXhbDWXtHADUFj+w==",
"requires": {
"@mapbox/jsonlint-lines-primitives": "2.0.1",
"@mapbox/unitbezier": "0.0.0",
@ -309,11 +304,6 @@
"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": {
"version": "0.1.0",
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
@ -6584,6 +6569,11 @@
"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": {
"version": "7.1.2",
"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": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz",
@ -8582,13 +8567,12 @@
}
},
"mapbox-gl": {
"version": "0.45.0",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-0.45.0.tgz",
"integrity": "sha1-r3HMgk8NflHM1cUF6q5BG8CRDM0=",
"version": "0.47.0",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-0.47.0.tgz",
"integrity": "sha512-y1AlNYMAKaqEtaqni0zOMYj9gTc1gZ0lqLkxXK9iFg5+ZBITc5DL9AcrXhpEXNxUzXKFa7dZkSULyNaqXFQ8yQ==",
"requires": {
"@mapbox/gl-matrix": "0.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/shelf-pack": "3.1.0",
"@mapbox/tiny-sdf": "1.1.0",
@ -8599,7 +8583,8 @@
"csscolorparser": "1.0.3",
"earcut": "2.1.3",
"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",
"grid-index": "1.0.0",
"minimist": "0.0.8",
@ -8608,10 +8593,35 @@
"rw": "1.3.3",
"shuffle-seed": "1.1.6",
"sort-object": "0.3.2",
"supercluster": "2.3.0",
"supercluster": "4.1.1",
"through2": "2.0.3",
"tinyqueue": "1.2.3",
"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": {
@ -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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",

View file

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

View file

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

View file

@ -1,22 +1,15 @@
import React from 'react'
import PropTypes from 'prop-types'
import FileReaderInput from 'react-file-reader-input'
import classnames from 'classnames'
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
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 SettingsIcon from 'react-icons/lib/md/settings'
import MdInfo from 'react-icons/lib/md/info'
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 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 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 pkgJson from '../../package.json'
import style from '../libs/style'
class IconText extends React.Component {
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 {
static propTypes = {
children: PropTypes.node,
@ -89,16 +103,13 @@ export default class Toolbar extends React.Component {
onToggleModal: PropTypes.func,
}
constructor(props) {
super(props)
this.state = {
isOpen: {
settings: false,
sources: false,
open: false,
add: false,
export: false,
}
state = {
isOpen: {
settings: false,
sources: false,
open: false,
add: false,
export: false,
}
}
@ -211,6 +222,10 @@ export default class Toolbar extends React.Component {
<HelpIcon />
<IconText>Help</IconText>
</ToolbarLink>
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
<SurveyIcon />
<IconText>Take the Maputnik Survey</IconText>
</ToolbarLinkHighlighted>
</div>
</div>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,30 @@ import docUid from '../../libs/document-uid'
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 {
static propTypes = {
onChange: PropTypes.func,
@ -29,45 +53,13 @@ export default class ZoomProperty extends React.Component {
]),
}
constructor() {
super()
this.state = {
refs: {}
}
state = {
refs: {}
}
componentDidMount() {
this.setState({
refs: this.setStopRefs(this.props)
})
}
const newRefs = setStopRefs(this.props, this.state);
/**
* 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) {
this.setState({
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.
orderStopsByZoom(stops) {
const mappedWithRef = stops

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,9 +16,6 @@ import LayerSourceLayerBlock from './LayerSourceLayerBlock'
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 layout from '../../config/layout.json'
@ -36,7 +33,7 @@ function layoutGroups(layerType) {
title: 'JSON Editor',
type: 'jsoneditor'
}
return [layerGroup, filterGroup].concat(layout[layerType].groups).concat([editorGroup])
return [layerGroup, filterGroup].concat(layout[layerType].groups).concat([editorGroup])
}
/** Layer editor supporting multiple types of layers. */
@ -79,18 +76,18 @@ export default class LayerEditor extends React.Component {
this.state = { editorGroups }
}
UNSAFE_componentWillReceiveProps(nextProps) {
const additionalGroups = { ...this.state.editorGroups }
static getDerivedStateFromProps(props, state) {
const additionalGroups = { ...state.editorGroups }
layout[nextProps.layer.type].groups.forEach(group => {
layout[props.layer.type].groups.forEach(group => {
if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true
}
})
this.setState({
return {
editorGroups: additionalGroups
})
};
}
getChildContext () {

View file

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

View file

@ -1,6 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import Color from 'color'
import classnames from 'classnames'
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 LayerIcon from '../icons/LayerIcon'
import LayerEditor from './LayerEditor'
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
@SortableHandle
@ -116,7 +114,7 @@ class LayerListItem extends React.Component {
/>
<IconAction
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)}
/>
</li>

View file

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

View file

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

View file

@ -5,7 +5,6 @@ import MapboxGl from 'mapbox-gl'
import MapboxInspect from 'mapbox-gl-inspect'
import FeatureLayerPopup from './FeatureLayerPopup'
import FeaturePropertyPopup from './FeaturePropertyPopup'
import style from '../../libs/style.js'
import tokens from '../../config/tokens.json'
import colors from 'mapbox-gl-inspect/lib/colors'
import Color from 'color'
@ -15,6 +14,9 @@ import 'mapbox-gl/dist/mapbox-gl.css'
import '../../mapboxgl.css'
import '../../libs/mapbox-rtl'
const IS_SUPPORTED = MapboxGl.supported();
function renderPropertyPopup(features) {
var mountNode = document.createElement('div');
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
@ -66,6 +68,7 @@ export default class MapboxGlMap extends React.Component {
onDataChange: () => {},
onLayerSelect: () => {},
mapboxAccessToken: tokens.mapbox,
options: {},
}
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
const metadata = nextProps.mapStyle.metadata || {}
const metadata = props.mapStyle.metadata || {}
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
//the necessary operations ourselves!
this.state.map.setStyle(nextProps.mapStyle, { diff: true})
this.state.map.setStyle(props.mapStyle, { diff: true})
}
}
componentDidUpdate(prevProps) {
if(!IS_SUPPORTED) return;
const map = this.state.map;
this.updateMapFromProps(this.props);
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
this.state.inspect.toggleInspector()
}
@ -102,10 +111,15 @@ export default class MapboxGlMap extends React.Component {
this.state.inspect.render()
}
map.showTileBoundaries = this.props.options.showTileBoundaries;
if (map) {
map.showTileBoundaries = this.props.options.showTileBoundaries;
map.showCollisionBoxes = this.props.options.showCollisionBoxes;
}
}
componentDidMount() {
if(!IS_SUPPORTED) return;
const mapOpts = {
...this.props.options,
container: this.container,
@ -116,6 +130,7 @@ export default class MapboxGlMap extends React.Component {
const map = new MapboxGl.Map(mapOpts);
map.showTileBoundaries = mapOpts.showTileBoundaries;
map.showCollisionBoxes = mapOpts.showCollisionBoxes;
const zoom = new ZoomControl;
map.addControl(zoom, 'top-right');
@ -161,9 +176,20 @@ export default class MapboxGlMap extends React.Component {
}
render() {
return <div
className="maputnik-map"
ref={x => this.container = x}
></div>
if(IS_SUPPORTED) {
return <div
className="maputnik-map__map"
ref={x => this.container = x}
></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 PropTypes from 'prop-types'
import style from '../../libs/style.js'
import isEqual from 'lodash.isequal'
import { loadJSON } from '../../libs/urlopen'
import 'ol/ol.css'
@ -29,10 +27,10 @@ class OpenLayers3Map extends React.Component {
const styleFunc = olms.apply(this.map, newMapStyle)
}
UNSAFE_componentWillReceiveProps(nextProps) {
componentDidUpdate() {
require.ensure(["ol", "ol-mapbox-style"], () => {
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 Button from '../Button'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import Modal from './Modal'
import LayerTypeBlock from '../layers/LayerTypeBlock'
@ -22,7 +20,7 @@ class AddModal extends React.Component {
sources: PropTypes.object.isRequired,
}
addLayer() {
addLayer = () => {
const changedLayers = this.props.layers.slice(0)
const layer = {
id: this.state.id,
@ -151,7 +149,7 @@ class AddModal extends React.Component {
}
<Button
className="maputnik-add-layer-button"
onClick={this.addLayer.bind(this)}
onClick={this.addLayer}
data-wd-key="add-layer"
>
Add Layer

View file

@ -10,207 +10,9 @@ import Button from '../Button'
import Modal from './Modal'
import MdFileDownload from 'react-icons/lib/md/file-download'
import TiClipboard from 'react-icons/lib/ti/clipboard'
import style from '../../libs/style.js'
import GitHub from 'github-api'
import { CopyToClipboard } from 'react-copy-to-clipboard'
import style from '../../libs/style'
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) {
const changedMetadata = { ...mapStyle.metadata }
@ -235,10 +37,24 @@ class ExportModal extends React.Component {
}
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");
}
changeMetadataProperty(property, value) {
const changedStyle = {
...this.props.mapStyle,
metadata: {
...this.props.mapStyle.metadata,
[property]: value
}
}
this.props.onStyleChanged(changedStyle)
}
render() {
return <Modal
data-wd-key="export-modal"
@ -252,16 +68,34 @@ class ExportModal extends React.Component {
<p>
Download a JSON style to your computer.
</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)}>
<MdFileDownload />
Download
</Button>
</div>
<div className="maputnik-modal-section hide">
<h4>Save style</h4>
<Gist mapStyle={this.props.mapStyle} onStyleChanged={this.props.onStyleChanged}/>
</div>
</Modal>
}
}

View file

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

View file

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

View file

@ -15,10 +15,6 @@ class SettingsModal extends React.Component {
onOpenToggle: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
}
changeStyleProperty(property, value) {
const changedStyle = {
...this.props.mapStyle,
@ -86,7 +82,7 @@ class SettingsModal extends React.Component {
/>
</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}
data-wd-key="modal-settings.maputnik:openmaptiles_access_token"
value={metadata['maputnik:openmaptiles_access_token']}
@ -94,12 +90,20 @@ class SettingsModal extends React.Component {
/>
</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."}>
<SelectInput {...inputProps}
data-wd-key="modal-settings.maputnik:renderer"
options={[
['mbgljs', 'MapboxGL JS'],
['ol3', 'Open Layers 3'],
// ['ol3', 'Open Layers 3'],
]}
value={metadata['maputnik:renderer'] || 'mbgljs'}
onChange={this.changeMetadataProperty.bind(this, 'maputnik:renderer')}

View file

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

View file

@ -236,9 +236,12 @@ class SourcesModal extends React.Component {
<p>
Add one of the publicly available sources to your style.
</p>
<div style={{maxwidth: 500}}>
<div className="maputnik-public-sources" style={{maxwidth: 500}}>
{tilesetOptions}
</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 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-saturation",
"raster-contrast",
"raster-fade-duration"
"raster-fade-duration",
"raster-resampling"
]
}
]

View file

@ -26,31 +26,13 @@
{
"id": "osm-liberty",
"title": "OSM Liberty",
"url": "https://rawgit.com/lukasmartinelli/osm-liberty/gh-pages/style.json",
"thumbnail": "https://cdn.rawgit.com/lukasmartinelli/osm-liberty/gh-pages/thumbnail.png"
"url": "https://rawgit.com/maputnik/osm-liberty/gh-pages/style.json",
"thumbnail": "https://cdn.rawgit.com/maputnik/osm-liberty/gh-pages/thumbnail.png"
},
{
"id": "empty-style",
"title": "Empty Style",
"url": "https://rawgit.com/maputnik/editor/master/src/config/empty-style.json",
"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": {
"type": "vector",
"url": "https://free.tilehosting.com/data/v3.json?key={key}",
"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",
"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.
const reducedMotionEnabled = lodash.throttle(() => {
const reducedMotionEnabled = throttle(() => {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches
}, 3000);

View file

@ -1,4 +1,3 @@
import request from 'request'
import style from './style.js'
import ReconnectingWebSocket from 'reconnecting-websocket'
@ -14,15 +13,20 @@ export class ApiStyleStore {
}
init(cb) {
request(localUrl + '/styles', (error, response, body) => {
if (!error && body && response.statusCode == 200) {
const styleIds = JSON.parse(body)
this.latestStyleId = styleIds[0]
this.notifyLocalChanges()
cb(null)
} else {
cb(new Error('Can not connect to style API'))
}
fetch(localUrl + '/styles', {
mode: 'cors',
})
.then(function(response) {
return response.json();
})
.then(function(body) {
const styleIds = body;
this.latestStyleId = styleIds[0]
this.notifyLocalChanges()
cb(null)
})
.catch(function() {
cb(new Error('Can not connect to style API'))
})
}
@ -44,8 +48,14 @@ export class ApiStyleStore {
latestStyle(cb) {
if(this.latestStyleId) {
request(localUrl + '/styles/' + this.latestStyleId, (error, response, body) => {
cb(style.ensureStyleValidity(JSON.parse(body)))
fetch(localUrl + '/styles/' + this.latestStyleId, {
mode: 'cors',
})
.then(function(response) {
return response.json();
})
.then(function(body) {
cb(style.ensureStyleValidity(body))
})
} else {
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(mapStyle) {
const id = mapStyle.id
request.put({
url: localUrl + '/styles/' + id,
json: true,
body: mapStyle
}, (error, response, body) => {
fetch(localUrl + '/styles/' + id, {
method: "PUT",
mode: 'cors',
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify(mapStyle)
})
.catch(function(error) {
if(error) console.error(error)
})
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.
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, {
type: "text/javascript"
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,31 @@
// MAP
.maputnik-map {
.maputnik-map__container {
display: flex;
position: fixed !important;
top: $toolbar-height + $toolbar-offset;
right: 0;
bottom: 0;
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
@ -56,6 +76,7 @@
border-width: 0;
border-radius: 2px;
box-sizing: border-box;
text-decoration: none;
&:hover {
background-color: lighten($color-midgray, 12);
@ -70,6 +91,20 @@
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 {
background-color: transparent;

View file

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

View file

@ -125,6 +125,10 @@
}
//SOURCE MODAL
.maputnik-public-sources {
margin-bottom: 1.5%;
}
.maputnik-public-source {
vertical-align: top;
margin-top: 1.5%;
@ -150,6 +154,7 @@
.maputnik-public-source-id {
font-weight: 400;
text-align: left;
}
.maputnik-active-source-type-editor {
@ -240,3 +245,21 @@
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>
.react-collapse-container {
display: flex;
max-width: 100%;
> * {
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 {
font-size: 10px;
margin-left: 4px;

View file

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

View file

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

View file

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

View file

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

View file

@ -171,7 +171,7 @@ describe("modals", function() {
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");
browser.selectByValue(selector, "ol3");
browser.click(wd.$("modal-settings.name"))

View file

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

View file

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