import autoBind from 'react-autobind'; import React from 'react' import cloneDeep from 'lodash.clonedeep' import clamp from 'lodash.clamp' import {arrayMove} from 'react-sortable-hoc' import url from 'url' import MapboxGlMap from './map/MapboxGlMap' import OpenLayersMap from './map/OpenLayersMap' import LayerList from './layers/LayerList' import LayerEditor from './layers/LayerEditor' import Toolbar from './Toolbar' import AppLayout from './AppLayout' import MessagePanel from './MessagePanel' 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' import SurveyModal from './modals/SurveyModal' import DebugModal from './modals/DebugModal' import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata' import {latest, validate} from '@mapbox/mapbox-gl-style-spec' import style from '../libs/style' import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen' import { undoMessages, redoMessages } from '../libs/diffmessage' import { StyleStore } from '../libs/stylestore' import { ApiStyleStore } from '../libs/apistore' import { RevisionStore } from '../libs/revisions' import LayerWatcher from '../libs/layerwatcher' import tokens from '../config/tokens.json' import isEqual from 'lodash.isequal' import Debug from '../libs/debug' import queryUtil from '../libs/query-util' import MapboxGl from 'mapbox-gl' // Similar functionality as 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; } } function setFetchAccessToken(url, mapStyle) { const matchesTilehosting = url.match(/\.tilehosting\.com/); const matchesMaptiler = url.match(/\.maptiler\.com/); const matchesThunderforest = url.match(/\.thunderforest\.com/); if (matchesTilehosting || matchesMaptiler) { 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; } } function updateRootSpec(spec, fieldName, newValues) { return { ...spec, $root: { ...spec.$root, [fieldName]: { ...spec.$root[fieldName], values: 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) }) const shortcuts = [ { key: "?", handler: () => { this.toggleModal("shortcuts"); } }, { key: "o", handler: () => { this.toggleModal("open"); } }, { key: "e", handler: () => { this.toggleModal("export"); } }, { key: "d", handler: () => { this.toggleModal("sources"); } }, { key: "s", handler: () => { this.toggleModal("settings"); } }, { key: "i", handler: () => { this.setMapState( this.state.mapState === "map" ? "inspect" : "map" ); } }, { key: "m", handler: () => { document.querySelector(".mapboxgl-canvas").focus(); } }, { key: "!", handler: () => { this.toggleModal("debug"); } }, ] document.body.addEventListener("keyup", (e) => { if(e.key === "Escape") { e.target.blur(); document.body.focus(); } else if(this.state.isOpen.shortcuts || document.activeElement === document.body) { const shortcut = shortcuts.find((shortcut) => { return (shortcut.key === e.key) }) if(shortcut) { this.setModal("shortcuts", false); shortcut.handler(e); } } }) const styleUrl = initialStyleUrl() if(styleUrl && window.confirm("Load style from URL: " + styleUrl + " and discard current changes?")) { this.styleStore = new StyleStore() loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle)) removeStyleQuerystring() } else { if(styleUrl) { removeStyleQuerystring() } 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)) if(Debug.enabled()) { Debug.set("maputnik", "styleStore", this.styleStore); Debug.set("maputnik", "revisionStore", this.revisionStore); } }) } if(Debug.enabled()) { Debug.set("maputnik", "revisionStore", this.revisionStore); Debug.set("maputnik", "styleStore", this.styleStore); } const queryObj = url.parse(window.location.href, true).query; this.state = { errors: [], infos: [], mapStyle: style.emptyStyle, selectedLayerIndex: 0, sources: {}, vectorLayers: {}, mapState: "map", spec: latest, isOpen: { settings: false, sources: false, open: false, shortcuts: false, export: false, survey: localStorage.hasOwnProperty('survey') ? false : true, debug: false, }, mapOptions: { showTileBoundaries: false, showCollisionBoxes: false, showOverdrawInspector: false, }, } this.layerWatcher = new LayerWatcher({ onVectorLayersChange: v => this.setState({ vectorLayers: v }) }) } 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() { window.addEventListener("keydown", this.handleKeyPress); } componentWillUnmount() { window.removeEventListener("keydown", this.handleKeyPress); } saveStyle(snapshotStyle) { this.styleStore.save(snapshotStyle) } updateFonts(urlTemplate) { const metadata = this.state.mapStyle.metadata || {} const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate; downloadGlyphsMetadata(glyphUrl, fonts => { this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)}) }) } updateIcons(baseUrl) { downloadSpriteMetadata(baseUrl, icons => { this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)}) }) } onStyleChanged = (newStyle, save=true) => { const errors = validate(newStyle, latest) if(errors.length === 0) { if(newStyle.glyphs !== this.state.mapStyle.glyphs) { this.updateFonts(newStyle.glyphs) } if(newStyle.sprite !== this.state.mapStyle.sprite) { this.updateIcons(newStyle.sprite) } this.revisionStore.addRevision(newStyle) if(save) this.saveStyle(newStyle) this.setState({ mapStyle: newStyle, errors: [], }) } else { this.setState({ errors: errors.map(err => err.message) }) } this.fetchSources(); } onUndo = () => { const activeStyle = this.revisionStore.undo() const messages = undoMessages(this.state.mapStyle, activeStyle) this.saveStyle(activeStyle) this.setState({ mapStyle: activeStyle, infos: messages, }) } onRedo = () => { const activeStyle = this.revisionStore.redo() const messages = redoMessages(this.state.mapStyle, activeStyle) this.saveStyle(activeStyle) this.setState({ mapStyle: activeStyle, infos: messages, }) } onMoveLayer = (move) => { 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); } onLayersChange = (changedLayers) => { const changedStyle = { ...this.state.mapStyle, layers: changedLayers } this.onStyleChanged(changedStyle) } onLayerDestroy = (layerId) => { let layers = this.state.mapStyle.layers; const remainingLayers = layers.slice(0); const idx = style.indexOfLayer(remainingLayers, layerId) remainingLayers.splice(idx, 1); this.onLayersChange(remainingLayers); } onLayerCopy = (layerId) => { 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) } onLayerVisibilityToggle = (layerId) => { 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) } onLayerIdChange = (oldId, newId) => { const changedLayers = this.state.mapStyle.layers.slice(0) const idx = style.indexOfLayer(changedLayers, oldId) changedLayers[idx] = { ...changedLayers[idx], id: newId } this.onLayersChange(changedLayers) } onLayerChanged = (layer) => { const changedLayers = this.state.mapStyle.layers.slice(0) const idx = style.indexOfLayer(changedLayers, layer.id) changedLayers[idx] = layer this.onLayersChange(changedLayers) } setMapState = (newState) => { this.setState({ mapState: newState }) } fetchSources() { const sourceList = {...this.state.sources}; for(let [key, val] of Object.entries(this.state.mapStyle.sources)) { if(sourceList.hasOwnProperty(key)) { continue; } sourceList[key] = { type: val.type, layers: [] }; if(!this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url")) { let url = val.url; try { url = normalizeSourceURL(url, MapboxGl.accessToken); } catch(err) { console.warn("Failed to normalizeSourceURL: ", err); } try { url = setFetchAccessToken(url, this.state.mapStyle) } catch(err) { console.warn("Failed to setFetchAccessToken: ", err); } fetch(url, { mode: 'cors', }) .then((response) => { return response.json(); }) .then((json) => { if(!json.hasOwnProperty("vector_layers")) { return; } // Create new objects before setState const sources = Object.assign({}, this.state.sources); for(let layer of json.vector_layers) { sources[key].layers.push(layer.id) } console.debug("Updating source: "+key); this.setState({ sources: sources }); }) .catch((err) => { console.error("Failed to process sources for '%s'", url, err); }) } } if(!isEqual(this.state.sources, sourceList)) { console.debug("Setting sources"); this.setState({ sources: sourceList }) } } mapRenderer() { const mapProps = { mapStyle: style.replaceAccessTokens(this.state.mapStyle, {allowFallback: true}), options: this.state.mapOptions, onDataChange: (e) => { this.layerWatcher.analyzeMap(e.map) this.fetchSources(); }, } const metadata = this.state.mapStyle.metadata || {} const renderer = metadata['maputnik:renderer'] || 'mbgljs' let mapElement; // Check if OL code has been loaded? if(renderer === 'ol') { mapElement = } else { mapElement = } let filterName; if(this.state.mapState.match(/^filter-/)) { filterName = this.state.mapState.replace(/^filter-/, ""); } const elementStyle = {}; if (filterName) { elementStyle.filter = `url('#${filterName}')`; }; return
{mapElement}
} onLayerSelect = (layerId) => { const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId) this.setState({ selectedLayerIndex: idx }) } setModal(modalName, value) { if(modalName === 'survey' && value === false) { localStorage.setItem('survey', ''); } this.setState({ isOpen: { ...this.state.isOpen, [modalName]: value } }) } toggleModal(modalName) { this.setModal(modalName, !this.state.isOpen[modalName]); } onChangeDebug = (key, value) => { this.setState({ mapOptions: { ...this.state.mapOptions, [key]: value, } }); } render() { const layers = this.state.mapStyle.layers || [] const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null const metadata = this.state.mapStyle.metadata || {} const toolbar = const layerList = const layerEditor = selectedLayer ? : null const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? : null const modals =
this.shortcutEl = el} isOpen={this.state.isOpen.shortcuts} onOpenToggle={this.toggleModal.bind(this, 'shortcuts')} />
return } }