2018-08-22 10:33:01 +02:00
|
|
|
import autoBind from 'react-autobind';
|
2016-09-09 00:10:54 +02:00
|
|
|
import React from 'react'
|
2018-05-22 22:16:46 +02:00
|
|
|
import cloneDeep from 'lodash.clonedeep'
|
|
|
|
import clamp from 'lodash.clamp'
|
2018-06-30 10:09:23 +02:00
|
|
|
import {arrayMove} from 'react-sortable-hoc'
|
2018-06-01 10:22:18 +02:00
|
|
|
import url from 'url'
|
2016-09-09 00:10:54 +02:00
|
|
|
|
2016-12-20 16:37:35 +01:00
|
|
|
import MapboxGlMap from './map/MapboxGlMap'
|
2018-09-24 15:05:53 +02:00
|
|
|
import OpenLayersMap from './map/OpenLayersMap'
|
2016-12-20 16:37:35 +01:00
|
|
|
import LayerList from './layers/LayerList'
|
|
|
|
import LayerEditor from './layers/LayerEditor'
|
|
|
|
import Toolbar from './Toolbar'
|
2016-12-22 16:39:09 +01:00
|
|
|
import AppLayout from './AppLayout'
|
2016-12-31 14:32:04 +01:00
|
|
|
import MessagePanel from './MessagePanel'
|
2016-09-09 00:10:54 +02:00
|
|
|
|
2018-05-29 18:06:00 +02:00
|
|
|
import SettingsModal from './modals/SettingsModal'
|
|
|
|
import ExportModal from './modals/ExportModal'
|
|
|
|
import SourcesModal from './modals/SourcesModal'
|
|
|
|
import OpenModal from './modals/OpenModal'
|
|
|
|
import ShortcutsModal from './modals/ShortcutsModal'
|
2018-06-25 19:52:48 +02:00
|
|
|
import SurveyModal from './modals/SurveyModal'
|
2019-05-18 19:54:10 +02:00
|
|
|
import DebugModal from './modals/DebugModal'
|
2018-05-29 18:06:00 +02:00
|
|
|
|
2017-01-10 11:13:53 +01:00
|
|
|
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
2018-10-06 22:05:33 +02:00
|
|
|
import {latest, validate} from '@mapbox/mapbox-gl-style-spec'
|
2018-07-27 17:25:53 +02:00
|
|
|
import style from '../libs/style'
|
2018-10-25 19:37:39 +02:00
|
|
|
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
|
2016-12-31 14:32:04 +01:00
|
|
|
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
2018-08-23 04:37:05 +02:00
|
|
|
import { StyleStore } from '../libs/stylestore'
|
2016-12-20 16:37:35 +01:00
|
|
|
import { ApiStyleStore } from '../libs/apistore'
|
2016-12-23 17:17:02 +01:00
|
|
|
import { RevisionStore } from '../libs/revisions'
|
2016-12-20 16:37:35 +01:00
|
|
|
import LayerWatcher from '../libs/layerwatcher'
|
2017-01-13 15:31:28 +01:00
|
|
|
import tokens from '../config/tokens.json'
|
2017-11-29 19:13:47 +01:00
|
|
|
import isEqual from 'lodash.isequal'
|
2018-01-05 18:45:55 +01:00
|
|
|
import Debug from '../libs/debug'
|
2018-06-03 18:33:08 +02:00
|
|
|
import queryUtil from '../libs/query-util'
|
2016-12-20 11:44:22 +01:00
|
|
|
|
2018-01-25 20:12:04 +01:00
|
|
|
import MapboxGl from 'mapbox-gl'
|
2018-10-08 10:31:28 +02:00
|
|
|
|
|
|
|
|
|
|
|
// Similar functionality as <https://github.com/mapbox/mapbox-gl-js/blob/7e30aadf5177486c2cfa14fe1790c60e217b5e56/src/util/mapbox.js>
|
|
|
|
function normalizeSourceURL (url, apiToken="") {
|
|
|
|
const matches = url.match(/^mapbox:\/\/(.*)/);
|
|
|
|
if (matches) {
|
|
|
|
// mapbox://mapbox.mapbox-streets-v7
|
|
|
|
return `https://api.mapbox.com/v4/${matches[1]}.json?secure&access_token=${apiToken}`
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
}
|
2018-01-25 20:12:04 +01:00
|
|
|
|
2018-11-27 13:29:47 +01:00
|
|
|
function setFetchAccessToken(url, mapStyle) {
|
2019-04-06 12:00:48 +02:00
|
|
|
const matchesTilehosting = url.match(/\.tilehosting\.com/);
|
2019-04-03 09:51:43 +02:00
|
|
|
const matchesMaptiler = url.match(/\.maptiler\.com/);
|
2018-11-27 13:29:47 +01:00
|
|
|
const matchesThunderforest = url.match(/\.thunderforest\.com/);
|
2019-04-06 12:00:48 +02:00
|
|
|
if (matchesTilehosting || matchesMaptiler) {
|
2018-11-27 13:29:47 +01:00
|
|
|
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
|
|
|
|
if (accessToken) {
|
|
|
|
return url.replace('{key}', accessToken)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (matchesThunderforest) {
|
|
|
|
const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true})
|
|
|
|
if (accessToken) {
|
|
|
|
return url.replace('{key}', accessToken)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
}
|
2018-01-25 20:12:04 +01:00
|
|
|
|
2017-01-10 11:13:53 +01:00
|
|
|
function updateRootSpec(spec, fieldName, newValues) {
|
|
|
|
return {
|
|
|
|
...spec,
|
|
|
|
$root: {
|
|
|
|
...spec.$root,
|
|
|
|
[fieldName]: {
|
|
|
|
...spec.$root[fieldName],
|
|
|
|
values: newValues
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-06-15 15:21:19 +02:00
|
|
|
|
2018-06-30 10:09:23 +02:00
|
|
|
export default class App extends React.Component {
|
2016-12-16 14:49:25 +01:00
|
|
|
constructor(props) {
|
|
|
|
super(props)
|
2018-08-22 10:33:01 +02:00
|
|
|
autoBind(this);
|
|
|
|
|
2016-12-23 17:17:02 +01:00
|
|
|
this.revisionStore = new RevisionStore()
|
2017-01-01 14:49:32 +01:00
|
|
|
this.styleStore = new ApiStyleStore({
|
|
|
|
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false)
|
|
|
|
})
|
2016-12-29 15:22:47 +01:00
|
|
|
|
2018-11-02 09:07:01 +01:00
|
|
|
|
2018-05-29 18:06:00 +02:00
|
|
|
const shortcuts = [
|
|
|
|
{
|
2018-10-30 21:12:48 +01:00
|
|
|
key: "?",
|
2018-05-29 18:06:00 +02:00
|
|
|
handler: () => {
|
|
|
|
this.toggleModal("shortcuts");
|
2018-05-28 13:03:47 +02:00
|
|
|
}
|
2018-05-29 18:06:00 +02:00
|
|
|
},
|
|
|
|
{
|
2018-10-30 21:12:48 +01:00
|
|
|
key: "o",
|
2018-05-29 18:06:00 +02:00
|
|
|
handler: () => {
|
|
|
|
this.toggleModal("open");
|
2018-05-28 13:03:47 +02:00
|
|
|
}
|
2018-05-29 18:06:00 +02:00
|
|
|
},
|
|
|
|
{
|
2018-10-30 21:12:48 +01:00
|
|
|
key: "e",
|
2018-05-29 18:06:00 +02:00
|
|
|
handler: () => {
|
|
|
|
this.toggleModal("export");
|
2018-05-28 13:03:47 +02:00
|
|
|
}
|
2018-05-29 18:06:00 +02:00
|
|
|
},
|
|
|
|
{
|
2018-10-30 21:12:48 +01:00
|
|
|
key: "d",
|
2018-05-29 18:06:00 +02:00
|
|
|
handler: () => {
|
|
|
|
this.toggleModal("sources");
|
2018-05-28 13:03:47 +02:00
|
|
|
}
|
2018-05-29 18:06:00 +02:00
|
|
|
},
|
|
|
|
{
|
2018-10-30 21:12:48 +01:00
|
|
|
key: "s",
|
2018-05-29 18:06:00 +02:00
|
|
|
handler: () => {
|
|
|
|
this.toggleModal("settings");
|
2018-05-28 13:03:47 +02:00
|
|
|
}
|
2018-05-29 18:06:00 +02:00
|
|
|
},
|
|
|
|
{
|
2018-10-30 21:12:48 +01:00
|
|
|
key: "i",
|
2018-05-29 18:06:00 +02:00
|
|
|
handler: () => {
|
2018-10-29 14:07:49 +01:00
|
|
|
this.setMapState(
|
|
|
|
this.state.mapState === "map" ? "inspect" : "map"
|
|
|
|
);
|
2018-05-28 13:03:47 +02:00
|
|
|
}
|
2018-05-29 18:06:00 +02:00
|
|
|
},
|
|
|
|
{
|
2018-10-30 21:12:48 +01:00
|
|
|
key: "m",
|
2018-05-29 18:06:00 +02:00
|
|
|
handler: () => {
|
|
|
|
document.querySelector(".mapboxgl-canvas").focus();
|
2018-05-28 13:03:47 +02:00
|
|
|
}
|
2018-05-29 18:06:00 +02:00
|
|
|
},
|
2019-05-18 19:54:10 +02:00
|
|
|
{
|
|
|
|
key: "!",
|
|
|
|
handler: () => {
|
|
|
|
this.toggleModal("debug");
|
|
|
|
}
|
|
|
|
},
|
2018-05-29 18:06:00 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
document.body.addEventListener("keyup", (e) => {
|
2018-10-30 21:12:48 +01:00
|
|
|
if(e.key === "Escape") {
|
2018-05-29 18:06:00 +02:00
|
|
|
e.target.blur();
|
|
|
|
document.body.focus();
|
|
|
|
}
|
2018-10-29 14:07:49 +01:00
|
|
|
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
|
2018-05-29 18:06:00 +02:00
|
|
|
const shortcut = shortcuts.find((shortcut) => {
|
2018-10-30 21:12:48 +01:00
|
|
|
return (shortcut.key === e.key)
|
2018-05-29 18:06:00 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
if(shortcut) {
|
2018-10-29 14:07:49 +01:00
|
|
|
this.setModal("shortcuts", false);
|
2018-05-29 18:06:00 +02:00
|
|
|
shortcut.handler(e);
|
2018-05-28 13:03:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2017-01-05 19:34:32 +01:00
|
|
|
const styleUrl = initialStyleUrl()
|
2018-10-29 17:35:12 +01:00
|
|
|
if(styleUrl && window.confirm("Load style from URL: " + styleUrl + " and discard current changes?")) {
|
|
|
|
this.styleStore = new StyleStore()
|
|
|
|
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
|
2018-10-25 19:37:39 +02:00
|
|
|
removeStyleQuerystring()
|
2017-01-05 19:34:32 +01:00
|
|
|
} else {
|
2018-10-29 17:35:12 +01:00
|
|
|
if(styleUrl) {
|
|
|
|
removeStyleQuerystring()
|
|
|
|
}
|
2017-01-05 19:34:32 +01:00
|
|
|
this.styleStore.init(err => {
|
|
|
|
if(err) {
|
|
|
|
console.log('Falling back to local storage for storing styles')
|
|
|
|
this.styleStore = new StyleStore()
|
|
|
|
}
|
|
|
|
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
2018-01-05 18:45:55 +01:00
|
|
|
|
|
|
|
if(Debug.enabled()) {
|
|
|
|
Debug.set("maputnik", "styleStore", this.styleStore);
|
|
|
|
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
|
|
|
}
|
2017-01-05 19:34:32 +01:00
|
|
|
})
|
|
|
|
}
|
2016-12-16 14:49:25 +01:00
|
|
|
|
2018-01-05 18:45:55 +01:00
|
|
|
if(Debug.enabled()) {
|
|
|
|
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
|
|
|
Debug.set("maputnik", "styleStore", this.styleStore);
|
|
|
|
}
|
|
|
|
|
2018-06-01 10:22:18 +02:00
|
|
|
const queryObj = url.parse(window.location.href, true).query;
|
|
|
|
|
2016-12-16 14:49:25 +01:00
|
|
|
this.state = {
|
2016-12-30 13:21:03 +01:00
|
|
|
errors: [],
|
2016-12-31 14:32:04 +01:00
|
|
|
infos: [],
|
2016-12-17 15:44:42 +01:00
|
|
|
mapStyle: style.emptyStyle,
|
2016-12-20 16:08:49 +01:00
|
|
|
selectedLayerIndex: 0,
|
2016-12-29 15:22:47 +01:00
|
|
|
sources: {},
|
|
|
|
vectorLayers: {},
|
2018-09-23 15:39:02 +02:00
|
|
|
mapState: "map",
|
2018-10-06 22:05:33 +02:00
|
|
|
spec: latest,
|
2018-05-29 18:06:00 +02:00
|
|
|
isOpen: {
|
|
|
|
settings: false,
|
|
|
|
sources: false,
|
|
|
|
open: false,
|
|
|
|
shortcuts: false,
|
|
|
|
export: false,
|
2019-05-18 19:54:10 +02:00
|
|
|
survey: localStorage.hasOwnProperty('survey') ? false : true,
|
|
|
|
debug: false,
|
2018-06-03 10:41:07 +02:00
|
|
|
},
|
2018-06-03 18:17:45 +02:00
|
|
|
mapOptions: {
|
2019-05-18 19:54:10 +02:00
|
|
|
showTileBoundaries: false,
|
|
|
|
showCollisionBoxes: false,
|
|
|
|
showOverdrawInspector: false,
|
2018-06-03 18:17:45 +02:00
|
|
|
},
|
2016-12-16 14:49:25 +01:00
|
|
|
}
|
2016-12-29 15:22:47 +01:00
|
|
|
|
|
|
|
this.layerWatcher = new LayerWatcher({
|
|
|
|
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
|
|
|
})
|
2016-12-16 14:49:25 +01:00
|
|
|
}
|
|
|
|
|
2018-08-22 10:33:01 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-23 17:17:02 +01:00
|
|
|
componentDidMount() {
|
2018-08-22 10:33:01 +02:00
|
|
|
window.addEventListener("keydown", this.handleKeyPress);
|
2016-12-23 17:17:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
2018-08-22 10:33:01 +02:00
|
|
|
window.removeEventListener("keydown", this.handleKeyPress);
|
2016-12-16 14:49:25 +01:00
|
|
|
}
|
|
|
|
|
2016-12-22 21:06:32 +01:00
|
|
|
saveStyle(snapshotStyle) {
|
2016-12-16 14:49:25 +01:00
|
|
|
this.styleStore.save(snapshotStyle)
|
|
|
|
}
|
|
|
|
|
2017-01-10 11:13:53 +01:00
|
|
|
updateFonts(urlTemplate) {
|
2017-01-13 15:31:28 +01:00
|
|
|
const metadata = this.state.mapStyle.metadata || {}
|
|
|
|
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
2018-02-01 22:44:15 +01:00
|
|
|
|
|
|
|
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
|
|
|
downloadGlyphsMetadata(glyphUrl, fonts => {
|
2017-01-10 11:13:53 +01:00
|
|
|
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
updateIcons(baseUrl) {
|
|
|
|
downloadSpriteMetadata(baseUrl, icons => {
|
|
|
|
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-08-23 03:09:37 +02:00
|
|
|
onStyleChanged = (newStyle, save=true) => {
|
2017-01-10 11:13:53 +01:00
|
|
|
|
2018-10-06 22:05:33 +02:00
|
|
|
const errors = validate(newStyle, latest)
|
2016-12-29 22:00:49 +01:00
|
|
|
if(errors.length === 0) {
|
2018-02-01 22:44:15 +01:00
|
|
|
|
|
|
|
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
|
|
|
this.updateFonts(newStyle.glyphs)
|
|
|
|
}
|
|
|
|
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
|
|
|
this.updateIcons(newStyle.sprite)
|
|
|
|
}
|
|
|
|
|
2016-12-29 22:00:49 +01:00
|
|
|
this.revisionStore.addRevision(newStyle)
|
2017-01-01 14:49:32 +01:00
|
|
|
if(save) this.saveStyle(newStyle)
|
2016-12-30 13:21:03 +01:00
|
|
|
this.setState({
|
|
|
|
mapStyle: newStyle,
|
|
|
|
errors: [],
|
|
|
|
})
|
2016-12-29 22:00:49 +01:00
|
|
|
} else {
|
2016-12-30 13:21:03 +01:00
|
|
|
this.setState({
|
|
|
|
errors: errors.map(err => err.message)
|
|
|
|
})
|
2016-12-29 22:00:49 +01:00
|
|
|
}
|
2017-11-08 12:11:36 +01:00
|
|
|
|
|
|
|
this.fetchSources();
|
2016-12-16 14:49:25 +01:00
|
|
|
}
|
|
|
|
|
2018-08-23 03:09:37 +02:00
|
|
|
onUndo = () => {
|
2016-12-23 17:17:02 +01:00
|
|
|
const activeStyle = this.revisionStore.undo()
|
2016-12-31 14:32:04 +01:00
|
|
|
const messages = undoMessages(this.state.mapStyle, activeStyle)
|
2016-12-23 17:17:02 +01:00
|
|
|
this.saveStyle(activeStyle)
|
2016-12-31 14:32:04 +01:00
|
|
|
this.setState({
|
|
|
|
mapStyle: activeStyle,
|
|
|
|
infos: messages,
|
|
|
|
})
|
2016-12-23 17:17:02 +01:00
|
|
|
}
|
|
|
|
|
2018-08-23 03:09:37 +02:00
|
|
|
onRedo = () => {
|
2016-12-23 17:17:02 +01:00
|
|
|
const activeStyle = this.revisionStore.redo()
|
2016-12-31 14:32:04 +01:00
|
|
|
const messages = redoMessages(this.state.mapStyle, activeStyle)
|
2016-12-23 17:17:02 +01:00
|
|
|
this.saveStyle(activeStyle)
|
2016-12-31 14:32:04 +01:00
|
|
|
this.setState({
|
|
|
|
mapStyle: activeStyle,
|
|
|
|
infos: messages,
|
|
|
|
})
|
2016-12-23 17:17:02 +01:00
|
|
|
}
|
|
|
|
|
2018-08-23 03:09:37 +02:00
|
|
|
onMoveLayer = (move) => {
|
2018-05-22 22:16:46 +02:00
|
|
|
let { oldIndex, newIndex } = move;
|
|
|
|
let layers = this.state.mapStyle.layers;
|
|
|
|
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
|
|
|
newIndex = clamp(newIndex, 0, layers.length-1);
|
|
|
|
if(oldIndex === newIndex) return;
|
|
|
|
|
|
|
|
if (oldIndex === this.state.selectedLayerIndex) {
|
|
|
|
this.setState({
|
|
|
|
selectedLayerIndex: newIndex
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
layers = layers.slice(0);
|
|
|
|
layers = arrayMove(layers, oldIndex, newIndex);
|
|
|
|
this.onLayersChange(layers);
|
|
|
|
}
|
|
|
|
|
2018-08-23 03:09:37 +02:00
|
|
|
onLayersChange = (changedLayers) => {
|
2016-12-20 16:08:49 +01:00
|
|
|
const changedStyle = {
|
|
|
|
...this.state.mapStyle,
|
2016-12-20 20:21:35 +01:00
|
|
|
layers: changedLayers
|
2016-12-20 16:08:49 +01:00
|
|
|
}
|
2016-12-22 21:06:32 +01:00
|
|
|
this.onStyleChanged(changedStyle)
|
2016-12-17 21:25:00 +01:00
|
|
|
}
|
|
|
|
|
2018-08-23 03:09:37 +02:00
|
|
|
onLayerDestroy = (layerId) => {
|
2018-05-22 22:16:46 +02:00
|
|
|
let layers = this.state.mapStyle.layers;
|
|
|
|
const remainingLayers = layers.slice(0);
|
|
|
|
const idx = style.indexOfLayer(remainingLayers, layerId)
|
|
|
|
remainingLayers.splice(idx, 1);
|
|
|
|
this.onLayersChange(remainingLayers);
|
|
|
|
}
|
|
|
|
|
2018-08-23 03:09:37 +02:00
|
|
|
onLayerCopy = (layerId) => {
|
2018-05-22 22:16:46 +02:00
|
|
|
let layers = this.state.mapStyle.layers;
|
|
|
|
const changedLayers = layers.slice(0)
|
|
|
|
const idx = style.indexOfLayer(changedLayers, layerId)
|
|
|
|
|
|
|
|
const clonedLayer = cloneDeep(changedLayers[idx])
|
|
|
|
clonedLayer.id = clonedLayer.id + "-copy"
|
|
|
|
changedLayers.splice(idx, 0, clonedLayer)
|
|
|
|
this.onLayersChange(changedLayers)
|
|
|
|
}
|
|
|
|
|
2018-08-23 03:09:37 +02:00
|
|
|
onLayerVisibilityToggle = (layerId) => {
|
2018-05-22 22:16:46 +02:00
|
|
|
let layers = this.state.mapStyle.layers;
|
|
|
|
const changedLayers = layers.slice(0)
|
|
|
|
const idx = style.indexOfLayer(changedLayers, layerId)
|
|
|
|
|
|
|
|
const layer = { ...changedLayers[idx] }
|
|
|
|
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
|
|
|
|
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
|
|
|
|
|
|
|
|
layer.layout = changedLayout
|
|
|
|
changedLayers[idx] = layer
|
|
|
|
this.onLayersChange(changedLayers)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-08-23 03:09:37 +02:00
|
|
|
onLayerIdChange = (oldId, newId) => {
|
2016-12-22 15:49:36 +01:00
|
|
|
const changedLayers = this.state.mapStyle.layers.slice(0)
|
|
|
|
const idx = style.indexOfLayer(changedLayers, oldId)
|
|
|
|
|
|
|
|
changedLayers[idx] = {
|
|
|
|
...changedLayers[idx],
|
|
|
|
id: newId
|
|
|
|
}
|
|
|
|
|
|
|
|
this.onLayersChange(changedLayers)
|
|
|
|
}
|
|
|
|
|
2018-08-23 03:09:37 +02:00
|
|
|
onLayerChanged = (layer) => {
|
2016-12-20 20:50:08 +01:00
|
|
|
const changedLayers = this.state.mapStyle.layers.slice(0)
|
|
|
|
const idx = style.indexOfLayer(changedLayers, layer.id)
|
|
|
|
changedLayers[idx] = layer
|
|
|
|
|
|
|
|
this.onLayersChange(changedLayers)
|
2016-12-17 21:25:00 +01:00
|
|
|
}
|
|
|
|
|
2018-09-23 12:39:15 +02:00
|
|
|
setMapState = (newState) => {
|
2017-01-08 22:03:21 +01:00
|
|
|
this.setState({
|
2018-06-18 21:28:24 +02:00
|
|
|
mapState: newState
|
2017-01-08 22:03:21 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-11-08 12:11:36 +01:00
|
|
|
fetchSources() {
|
2017-11-29 19:13:47 +01:00
|
|
|
const sourceList = {...this.state.sources};
|
2017-11-08 12:11:36 +01:00
|
|
|
|
|
|
|
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
2017-11-29 19:13:47 +01:00
|
|
|
if(sourceList.hasOwnProperty(key)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2017-11-17 12:43:56 +01:00
|
|
|
sourceList[key] = {
|
|
|
|
type: val.type,
|
|
|
|
layers: []
|
|
|
|
};
|
2017-11-08 12:11:36 +01:00
|
|
|
|
2018-02-01 22:33:07 +01:00
|
|
|
if(!this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url")) {
|
2018-01-25 20:40:54 +01:00
|
|
|
let url = val.url;
|
|
|
|
try {
|
2018-07-12 13:33:40 +02:00
|
|
|
url = normalizeSourceURL(url, MapboxGl.accessToken);
|
2018-01-25 20:40:54 +01:00
|
|
|
} catch(err) {
|
|
|
|
console.warn("Failed to normalizeSourceURL: ", err);
|
|
|
|
}
|
2018-01-25 20:12:04 +01:00
|
|
|
|
2018-11-27 13:29:47 +01:00
|
|
|
try {
|
|
|
|
url = setFetchAccessToken(url, this.state.mapStyle)
|
|
|
|
} catch(err) {
|
|
|
|
console.warn("Failed to setFetchAccessToken: ", err);
|
|
|
|
}
|
|
|
|
|
2018-08-22 10:33:01 +02:00
|
|
|
fetch(url, {
|
|
|
|
mode: 'cors',
|
|
|
|
})
|
2017-11-08 12:11:36 +01:00
|
|
|
.then((response) => {
|
|
|
|
return response.json();
|
|
|
|
})
|
|
|
|
.then((json) => {
|
2018-02-01 22:33:07 +01:00
|
|
|
if(!json.hasOwnProperty("vector_layers")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-11-17 11:53:46 +01:00
|
|
|
// Create new objects before setState
|
2017-11-29 19:13:47 +01:00
|
|
|
const sources = Object.assign({}, this.state.sources);
|
2017-11-08 12:11:36 +01:00
|
|
|
|
|
|
|
for(let layer of json.vector_layers) {
|
2017-11-29 19:13:47 +01:00
|
|
|
sources[key].layers.push(layer.id)
|
2017-11-08 12:11:36 +01:00
|
|
|
}
|
|
|
|
|
2017-11-29 19:13:47 +01:00
|
|
|
console.debug("Updating source: "+key);
|
2017-11-08 12:11:36 +01:00
|
|
|
this.setState({
|
2017-11-29 19:13:47 +01:00
|
|
|
sources: sources
|
2017-11-08 12:11:36 +01:00
|
|
|
});
|
|
|
|
})
|
|
|
|
.catch((err) => {
|
|
|
|
console.error("Failed to process sources for '%s'", url, err);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-29 19:13:47 +01:00
|
|
|
if(!isEqual(this.state.sources, sourceList)) {
|
|
|
|
console.debug("Setting sources");
|
|
|
|
this.setState({
|
|
|
|
sources: sourceList
|
|
|
|
})
|
|
|
|
}
|
2017-11-08 12:11:36 +01:00
|
|
|
}
|
|
|
|
|
2016-12-17 14:53:16 +01:00
|
|
|
mapRenderer() {
|
2016-12-16 15:14:20 +01:00
|
|
|
const mapProps = {
|
2018-07-27 17:25:53 +02:00
|
|
|
mapStyle: style.replaceAccessTokens(this.state.mapStyle, {allowFallback: true}),
|
2018-06-03 18:17:45 +02:00
|
|
|
options: this.state.mapOptions,
|
2016-12-24 14:42:57 +01:00
|
|
|
onDataChange: (e) => {
|
|
|
|
this.layerWatcher.analyzeMap(e.map)
|
2017-11-08 12:11:36 +01:00
|
|
|
this.fetchSources();
|
2016-12-24 15:14:31 +01:00
|
|
|
},
|
2016-12-16 15:14:20 +01:00
|
|
|
}
|
2016-12-20 16:08:49 +01:00
|
|
|
|
2017-01-13 15:31:28 +01:00
|
|
|
const metadata = this.state.mapStyle.metadata || {}
|
2016-12-20 16:08:49 +01:00
|
|
|
const renderer = metadata['maputnik:renderer'] || 'mbgljs'
|
2016-12-22 18:08:42 +01:00
|
|
|
|
2018-06-01 10:22:18 +02:00
|
|
|
let mapElement;
|
|
|
|
|
2018-09-24 22:01:37 +02:00
|
|
|
// Check if OL code has been loaded?
|
2018-09-24 15:05:53 +02:00
|
|
|
if(renderer === 'ol') {
|
|
|
|
mapElement = <OpenLayersMap
|
|
|
|
{...mapProps}
|
|
|
|
/>
|
2016-12-17 14:53:16 +01:00
|
|
|
} else {
|
2018-06-01 10:22:18 +02:00
|
|
|
mapElement = <MapboxGlMap {...mapProps}
|
2018-06-18 21:28:24 +02:00
|
|
|
inspectModeEnabled={this.state.mapState === "inspect"}
|
2018-01-08 22:18:30 +01:00
|
|
|
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
2018-08-23 03:09:37 +02:00
|
|
|
onLayerSelect={this.onLayerSelect} />
|
2016-12-17 14:53:16 +01:00
|
|
|
}
|
2018-06-01 10:22:18 +02:00
|
|
|
|
2018-09-23 12:39:15 +02:00
|
|
|
let filterName;
|
2018-06-18 21:28:24 +02:00
|
|
|
if(this.state.mapState.match(/^filter-/)) {
|
|
|
|
filterName = this.state.mapState.replace(/^filter-/, "");
|
2018-07-17 21:40:23 +02:00
|
|
|
}
|
2018-09-23 12:39:15 +02:00
|
|
|
const elementStyle = {};
|
|
|
|
if (filterName) {
|
2018-09-23 15:48:19 +02:00
|
|
|
elementStyle.filter = `url('#${filterName}')`;
|
2018-06-01 10:22:18 +02:00
|
|
|
};
|
|
|
|
|
2018-09-23 12:39:15 +02:00
|
|
|
return <div style={elementStyle} className="maputnik-map__container">
|
2018-06-01 10:22:18 +02:00
|
|
|
{mapElement}
|
|
|
|
</div>
|
2016-12-17 14:53:16 +01:00
|
|
|
}
|
|
|
|
|
2018-08-23 03:09:37 +02:00
|
|
|
onLayerSelect = (layerId) => {
|
2016-12-20 20:21:35 +01:00
|
|
|
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId)
|
|
|
|
this.setState({ selectedLayerIndex: idx })
|
2016-12-17 16:09:37 +01:00
|
|
|
}
|
|
|
|
|
2018-10-29 14:07:49 +01:00
|
|
|
setModal(modalName, value) {
|
|
|
|
if(modalName === 'survey' && value === false) {
|
|
|
|
localStorage.setItem('survey', '');
|
|
|
|
}
|
|
|
|
|
2018-05-29 18:06:00 +02:00
|
|
|
this.setState({
|
|
|
|
isOpen: {
|
|
|
|
...this.state.isOpen,
|
2018-10-29 14:07:49 +01:00
|
|
|
[modalName]: value
|
2018-05-29 18:06:00 +02:00
|
|
|
}
|
|
|
|
})
|
2018-10-29 14:07:49 +01:00
|
|
|
}
|
2018-06-25 19:52:48 +02:00
|
|
|
|
2018-10-29 14:07:49 +01:00
|
|
|
toggleModal(modalName) {
|
|
|
|
this.setModal(modalName, !this.state.isOpen[modalName]);
|
2018-05-29 18:06:00 +02:00
|
|
|
}
|
|
|
|
|
2019-05-18 19:54:10 +02:00
|
|
|
onChangeDebug = (key, value) => {
|
|
|
|
this.setState({
|
|
|
|
mapOptions: {
|
|
|
|
...this.state.mapOptions,
|
|
|
|
[key]: value,
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-12-17 14:53:16 +01:00
|
|
|
render() {
|
2016-12-20 16:08:49 +01:00
|
|
|
const layers = this.state.mapStyle.layers || []
|
|
|
|
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
2016-12-28 17:08:42 +01:00
|
|
|
const metadata = this.state.mapStyle.metadata || {}
|
2016-12-20 14:50:38 +01:00
|
|
|
|
2016-12-20 16:37:35 +01:00
|
|
|
const toolbar = <Toolbar
|
2018-06-18 21:28:24 +02:00
|
|
|
mapState={this.state.mapState}
|
2016-12-20 16:37:35 +01:00
|
|
|
mapStyle={this.state.mapStyle}
|
2018-06-18 21:28:24 +02:00
|
|
|
inspectModeEnabled={this.state.mapState === "inspect"}
|
2016-12-29 15:22:47 +01:00
|
|
|
sources={this.state.sources}
|
2018-08-23 03:09:37 +02:00
|
|
|
onStyleChanged={this.onStyleChanged}
|
|
|
|
onStyleOpen={this.onStyleChanged}
|
2018-09-23 12:39:15 +02:00
|
|
|
onSetMapState={this.setMapState}
|
2018-05-29 18:06:00 +02:00
|
|
|
onToggleModal={this.toggleModal.bind(this)}
|
2016-12-20 16:37:35 +01:00
|
|
|
/>
|
|
|
|
|
|
|
|
const layerList = <LayerList
|
2018-08-23 03:09:37 +02:00
|
|
|
onMoveLayer={this.onMoveLayer}
|
|
|
|
onLayerDestroy={this.onLayerDestroy}
|
|
|
|
onLayerCopy={this.onLayerCopy}
|
|
|
|
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
|
|
|
onLayersChange={this.onLayersChange}
|
|
|
|
onLayerSelect={this.onLayerSelect}
|
2016-12-20 19:20:56 +01:00
|
|
|
selectedLayerIndex={this.state.selectedLayerIndex}
|
2016-12-20 16:37:35 +01:00
|
|
|
layers={layers}
|
2017-01-11 15:59:51 +01:00
|
|
|
sources={this.state.sources}
|
2016-12-20 16:37:35 +01:00
|
|
|
/>
|
|
|
|
|
|
|
|
const layerEditor = selectedLayer ? <LayerEditor
|
|
|
|
layer={selectedLayer}
|
2018-05-22 22:16:46 +02:00
|
|
|
layerIndex={this.state.selectedLayerIndex}
|
|
|
|
isFirstLayer={this.state.selectedLayerIndex < 1}
|
|
|
|
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1}
|
2016-12-29 15:22:47 +01:00
|
|
|
sources={this.state.sources}
|
|
|
|
vectorLayers={this.state.vectorLayers}
|
2017-01-10 11:13:53 +01:00
|
|
|
spec={this.state.spec}
|
2018-08-23 03:09:37 +02:00
|
|
|
onMoveLayer={this.onMoveLayer}
|
|
|
|
onLayerChanged={this.onLayerChanged}
|
|
|
|
onLayerDestroy={this.onLayerDestroy}
|
|
|
|
onLayerCopy={this.onLayerCopy}
|
|
|
|
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
|
|
|
onLayerIdChange={this.onLayerIdChange}
|
2016-12-20 16:37:35 +01:00
|
|
|
/> : null
|
|
|
|
|
2016-12-31 14:32:04 +01:00
|
|
|
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
|
|
|
errors={this.state.errors}
|
|
|
|
infos={this.state.infos}
|
2016-12-30 13:21:03 +01:00
|
|
|
/> : null
|
|
|
|
|
2018-05-29 18:06:00 +02:00
|
|
|
|
|
|
|
const modals = <div>
|
2019-05-18 19:54:10 +02:00
|
|
|
<DebugModal
|
|
|
|
debugOptions={this.state.mapOptions}
|
|
|
|
onChangeDebug={this.onChangeDebug}
|
|
|
|
isOpen={this.state.isOpen.debug}
|
|
|
|
onOpenToggle={this.toggleModal.bind(this, 'debug')}
|
|
|
|
/>
|
2018-05-29 18:06:00 +02:00
|
|
|
<ShortcutsModal
|
2018-10-29 14:07:49 +01:00
|
|
|
ref={(el) => this.shortcutEl = el}
|
2018-05-29 18:06:00 +02:00
|
|
|
isOpen={this.state.isOpen.shortcuts}
|
|
|
|
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
|
|
|
/>
|
|
|
|
<SettingsModal
|
|
|
|
mapStyle={this.state.mapStyle}
|
2018-08-23 03:09:37 +02:00
|
|
|
onStyleChanged={this.onStyleChanged}
|
2018-05-29 18:06:00 +02:00
|
|
|
isOpen={this.state.isOpen.settings}
|
|
|
|
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
|
|
|
/>
|
|
|
|
<ExportModal
|
|
|
|
mapStyle={this.state.mapStyle}
|
2018-08-23 03:09:37 +02:00
|
|
|
onStyleChanged={this.onStyleChanged}
|
2018-05-29 18:06:00 +02:00
|
|
|
isOpen={this.state.isOpen.export}
|
|
|
|
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
|
|
|
/>
|
|
|
|
<OpenModal
|
|
|
|
isOpen={this.state.isOpen.open}
|
2018-08-23 03:09:37 +02:00
|
|
|
onStyleOpen={this.onStyleChanged}
|
2018-05-29 18:06:00 +02:00
|
|
|
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
|
|
|
/>
|
|
|
|
<SourcesModal
|
|
|
|
mapStyle={this.state.mapStyle}
|
2018-08-23 03:09:37 +02:00
|
|
|
onStyleChanged={this.onStyleChanged}
|
2018-05-29 18:06:00 +02:00
|
|
|
isOpen={this.state.isOpen.sources}
|
|
|
|
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
|
|
|
/>
|
2018-06-25 19:52:48 +02:00
|
|
|
<SurveyModal
|
|
|
|
isOpen={this.state.isOpen.survey}
|
|
|
|
onOpenToggle={this.toggleModal.bind(this, 'survey')}
|
|
|
|
/>
|
2018-05-29 18:06:00 +02:00
|
|
|
</div>
|
|
|
|
|
2016-12-22 16:39:09 +01:00
|
|
|
return <AppLayout
|
2016-12-20 16:37:35 +01:00
|
|
|
toolbar={toolbar}
|
|
|
|
layerList={layerList}
|
|
|
|
layerEditor={layerEditor}
|
|
|
|
map={this.mapRenderer()}
|
2016-12-30 13:21:03 +01:00
|
|
|
bottom={bottomPanel}
|
2018-05-29 18:06:00 +02:00
|
|
|
modals={modals}
|
2016-12-20 16:37:35 +01:00
|
|
|
/>
|
2016-12-16 14:49:25 +01:00
|
|
|
}
|
2016-09-08 19:47:29 +02:00
|
|
|
}
|