diff --git a/package-lock.json b/package-lock.json index 133f90199..bb3cda311 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,10 +73,15 @@ "@types/color": "^3.0.6", "@types/cors": "^2.8.17", "@types/file-saver": "^2.0.7", + "@types/json-to-ast": "^2.1.4", "@types/lodash.capitalize": "^4.2.9", + "@types/lodash.clamp": "^4.0.9", + "@types/lodash.clonedeep": "^4.5.9", + "@types/lodash.get": "^4.4.9", "@types/lodash.isequal": "^4.5.8", "@types/lodash.throttle": "^4.1.9", "@types/react": "^16.14.52", + "@types/react-aria-menubutton": "^6.2.13", "@types/react-aria-modal": "^4.0.9", "@types/react-autocomplete": "^1.8.9", "@types/react-collapse": "^5.0.4", @@ -84,6 +89,7 @@ "@types/react-dom": "^16.9.24", "@types/react-file-reader-input": "^2.0.4", "@types/react-icon-base": "^2.1.6", + "@types/string-hash": "^1.1.3", "@types/uuid": "^9.0.7", "@vitejs/plugin-react": "^4.2.0", "cors": "^2.8.5", @@ -4889,6 +4895,12 @@ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, + "node_modules/@types/json-to-ast": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/json-to-ast/-/json-to-ast-2.1.4.tgz", + "integrity": "sha512-131wOmuwDg8ypYCSQ437bGdP+K2lJ8GJUu+ng4iQQxAc3irRnb7mGHbexsPChBcKWLctTR9V5LJdX5A8WWk44A==", + "dev": true + }, "node_modules/@types/lodash": { "version": "4.14.202", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", @@ -4904,6 +4916,33 @@ "@types/lodash": "*" } }, + "node_modules/@types/lodash.clamp": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.clamp/-/lodash.clamp-4.0.9.tgz", + "integrity": "sha512-t+hBIPHXyBVYkl0KEAEchOJwBrG8czt3E7r5fdpwMRrn3g+hkRzw6cjzWl+nJg3Z2QqRaQLt+W2n4ikwGr1u2g==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.clonedeep": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.9.tgz", + "integrity": "sha512-19429mWC+FyaAhOLzsS8kZUsI+/GmBAQ0HFiCPsKGU+7pBXOQWhyrY6xNNDwUSX8SMZMJvuFVMF9O5dQOlQK9Q==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/lodash.get": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@types/lodash.get/-/lodash.get-4.4.9.tgz", + "integrity": "sha512-J5dvW98sxmGnamqf+/aLP87PYXyrha9xIgc2ZlHl6OHMFR2Ejdxep50QfU0abO1+CH6+ugx+8wEUN1toImAinA==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/lodash.isequal": { "version": "4.5.8", "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz", @@ -5032,6 +5071,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-aria-menubutton": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/@types/react-aria-menubutton/-/react-aria-menubutton-6.2.13.tgz", + "integrity": "sha512-olSjeIzNzn0KrbShOmBwchHS++khDXBYFTO2U802o8LDHANLms7zUsJhdecfqFpwdFMHxFiMMlCn2nJNCEHWlQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-aria-modal": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/react-aria-modal/-/react-aria-modal-4.0.9.tgz", @@ -5156,6 +5204,12 @@ "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", "dev": true }, + "node_modules/@types/string-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/string-hash/-/string-hash-1.1.3.tgz", + "integrity": "sha512-p6skq756fJWiA59g2Uss+cMl6tpoDGuCBuxG0SI1t0NwJmYOU66LAMS6QiCgu7cUh3/hYCaMl5phcCW1JP5wOA==", + "dev": true + }, "node_modules/@types/tern": { "version": "0.23.9", "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", diff --git a/package.json b/package.json index c6c580b1b..931f74ecc 100644 --- a/package.json +++ b/package.json @@ -102,10 +102,15 @@ "@types/color": "^3.0.6", "@types/cors": "^2.8.17", "@types/file-saver": "^2.0.7", + "@types/json-to-ast": "^2.1.4", "@types/lodash.capitalize": "^4.2.9", + "@types/lodash.clamp": "^4.0.9", + "@types/lodash.clonedeep": "^4.5.9", + "@types/lodash.get": "^4.4.9", "@types/lodash.isequal": "^4.5.8", "@types/lodash.throttle": "^4.1.9", "@types/react": "^16.14.52", + "@types/react-aria-menubutton": "^6.2.13", "@types/react-aria-modal": "^4.0.9", "@types/react-autocomplete": "^1.8.9", "@types/react-collapse": "^5.0.4", @@ -113,6 +118,7 @@ "@types/react-dom": "^16.9.24", "@types/react-file-reader-input": "^2.0.4", "@types/react-icon-base": "^2.1.6", + "@types/string-hash": "^1.1.3", "@types/uuid": "^9.0.7", "@vitejs/plugin-react": "^4.2.0", "cors": "^2.8.5", diff --git a/src/components/App.jsx b/src/components/App.tsx similarity index 81% rename from src/components/App.jsx rename to src/components/App.tsx index 4287f8ebf..f02a416c7 100644 --- a/src/components/App.jsx +++ b/src/components/App.tsx @@ -1,3 +1,4 @@ +// @ts-ignore - this can be easily replaced with arrow functions import autoBind from 'react-autobind'; import React from 'react' import cloneDeep from 'lodash.clonedeep' @@ -6,14 +7,15 @@ import buffer from 'buffer' import get from 'lodash.get' import {unset} from 'lodash' import {arrayMoveMutable} from 'array-move' -import url from 'url' import hash from "string-hash"; +import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl' +import {latest, validate} from '@maplibre/maplibre-gl-style-spec' import MapMaplibreGl from './MapMaplibreGl' import MapOpenLayers from './MapOpenLayers' import LayerList from './LayerList' import LayerEditor from './LayerEditor' -import AppToolbar from './AppToolbar' +import AppToolbar, { MapState } from './AppToolbar' import AppLayout from './AppLayout' import MessagePanel from './AppMessagePanel' @@ -25,8 +27,7 @@ import ModalShortcuts from './ModalShortcuts' import ModalSurvey from './ModalSurvey' import ModalDebug from './ModalDebug' -import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata' -import {latest, validate} from '@maplibre/maplibre-gl-style-spec' +import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata' import style from '../libs/style' import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen' import { undoMessages, redoMessages } from '../libs/diffmessage' @@ -37,12 +38,13 @@ import LayerWatcher from '../libs/layerwatcher' import tokens from '../config/tokens.json' import isEqual from 'lodash.isequal' import Debug from '../libs/debug' -import {formatLayerId} from '../util/format'; +import { SortEnd } from 'react-sortable-hoc'; +import { MapOptions } from 'maplibre-gl'; // Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed. window.Buffer = buffer.Buffer; -function setFetchAccessToken(url, mapStyle) { +function setFetchAccessToken(url: string, mapStyle: StyleSpecification) { const matchesTilehosting = url.match(/\.tilehosting\.com/); const matchesMaptiler = url.match(/\.maptiler\.com/); const matchesThunderforest = url.match(/\.thunderforest\.com/); @@ -63,7 +65,7 @@ function setFetchAccessToken(url, mapStyle) { } } -function updateRootSpec(spec, fieldName, newValues) { +function updateRootSpec(spec: any, fieldName: string, newValues: any) { return { ...spec, $root: { @@ -76,15 +78,75 @@ function updateRootSpec(spec, fieldName, newValues) { } } -export default class App extends React.Component { - constructor(props) { +type OnStyleChangedOpts = { + save?: boolean + addRevision?: boolean + initialLoad?: boolean +} + +type MappedErrors = { + message: string + parsed?: { + type: string + data: { + index: number + key: string + message: string + } + } +} + +type AppState = { + errors: MappedErrors[], + infos: string[], + mapStyle: StyleSpecification & {id: string}, + dirtyMapStyle?: StyleSpecification, + selectedLayerIndex: number, + selectedLayerOriginalId?: string, + sources: {[key: string]: SourceSpecification}, + vectorLayers: {}, + spec: any, + mapView: { + zoom: number, + center: { + lng: number, + lat: number, + }, + }, + maplibreGlDebugOptions: Partial & { + showTileBoundaries: boolean, + showCollisionBoxes: boolean, + showOverdrawInspector: boolean, + }, + openlayersDebugOptions: { + debugToolbox: boolean, + }, + mapState: MapState + isOpen: { + settings: boolean + sources: boolean + open: boolean + shortcuts: boolean + export: boolean + survey: boolean + debug: boolean + } +} + +export default class App extends React.Component { + revisionStore: RevisionStore; + styleStore: StyleStore | ApiStyleStore; + layerWatcher: LayerWatcher; + shortcutEl: ModalShortcuts | null = null; + + constructor(props: any) { super(props) autoBind(this); this.revisionStore = new RevisionStore() const params = new URLSearchParams(window.location.search.substring(1)) let port = params.get("localport") - if (port == null && (window.location.port != 80 && window.location.port != 443)) { + if (port == null && (window.location.port !== "80" && window.location.port !== "443")) { port = window.location.port } this.styleStore = new ApiStyleStore({ @@ -136,7 +198,7 @@ export default class App extends React.Component { { key: "m", handler: () => { - document.querySelector(".maplibregl-canvas").focus(); + (document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus(); } }, { @@ -149,7 +211,7 @@ export default class App extends React.Component { document.body.addEventListener("keyup", (e) => { if(e.key === "Escape") { - e.target.blur(); + (e.target as HTMLElement).blur(); document.body.focus(); } else if(this.state.isOpen.shortcuts || document.activeElement === document.body) { @@ -159,7 +221,7 @@ export default class App extends React.Component { if(shortcut) { this.setModal("shortcuts", false); - shortcut.handler(e); + shortcut.handler(); } } }) @@ -192,8 +254,6 @@ export default class App extends React.Component { Debug.set("maputnik", "styleStore", this.styleStore); } - const queryObj = url.parse(window.location.href, true).query; - this.state = { errors: [], infos: [], @@ -235,25 +295,25 @@ export default class App extends React.Component { }) } - handleKeyPress = (e) => { + handleKeyPress = (e: KeyboardEvent) => { if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) { if(e.metaKey && e.shiftKey && e.keyCode === 90) { e.preventDefault(); - this.onRedo(e); + this.onRedo(); } else if(e.metaKey && e.keyCode === 90) { e.preventDefault(); - this.onUndo(e); + this.onUndo(); } } else { if(e.ctrlKey && e.keyCode === 90) { e.preventDefault(); - this.onUndo(e); + this.onUndo(); } else if(e.ctrlKey && e.keyCode === 89) { e.preventDefault(); - this.onRedo(e); + this.onRedo(); } } } @@ -266,12 +326,12 @@ export default class App extends React.Component { window.removeEventListener("keydown", this.handleKeyPress); } - saveStyle(snapshotStyle) { + saveStyle(snapshotStyle: StyleSpecification & {id: string}) { this.styleStore.save(snapshotStyle) } - updateFonts(urlTemplate) { - const metadata = this.state.mapStyle.metadata || {} + updateFonts(urlTemplate: string) { + const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate; @@ -280,13 +340,13 @@ export default class App extends React.Component { }) } - updateIcons(baseUrl) { + updateIcons(baseUrl: string) { downloadSpriteMetadata(baseUrl, icons => { this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)}) }) } - onChangeMetadataProperty = (property, value) => { + onChangeMetadataProperty = (property: string, value: any) => { // If we're changing renderer reset the map state. if ( property === 'maputnik:renderer' && @@ -300,14 +360,14 @@ export default class App extends React.Component { const changedStyle = { ...this.state.mapStyle, metadata: { - ...this.state.mapStyle.metadata, + ...(this.state.mapStyle as any).metadata, [property]: value } } this.onStyleChanged(changedStyle) } - onStyleChanged = (newStyle, opts={}) => { + onStyleChanged = (newStyle: StyleSpecification & {id: string}, opts: OnStyleChangedOpts={}) => { opts = { save: true, addRevision: true, @@ -319,16 +379,16 @@ export default class App extends React.Component { this.getInitialStateFromUrl(newStyle); } - const errors = validate(newStyle, latest) || []; + // This "any" can be removed in latest version of maplibre where maplibre re-exported types from style-spec + const errors = validate(newStyle as any, latest) || []; // The validate function doesn't give us errors for duplicate error with // empty string for layer.id, manually deal with that here. - const layerErrors = []; + const layerErrors: (Error | ValidationError)[] = []; if (newStyle && newStyle.layers) { - const foundLayers = new Map(); + const foundLayers = new global.Map(); newStyle.layers.forEach((layer, index) => { if (layer.id === "" && foundLayers.has(layer.id)) { - const message = `Duplicate layer: ${formatLayerId(layer.id)}`; const error = new Error( `layers[${index}]: duplicate layer id [empty_string], previously used` ); @@ -342,7 +402,7 @@ export default class App extends React.Component { // Special case: Duplicate layer id const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/); if (dupMatch) { - const [matchStr, index, message] = dupMatch; + const [_matchStr, index, message] = dupMatch; return { message: error.message, parsed: { @@ -359,7 +419,7 @@ export default class App extends React.Component { // Special case: Invalid source const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/); if (invalidSourceMatch) { - const [matchStr, index, message] = invalidSourceMatch; + const [_matchStr, index, message] = invalidSourceMatch; return { message: error.message, parsed: { @@ -375,7 +435,7 @@ export default class App extends React.Component { const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/); if (layerMatch) { - const [matchStr, index, group, property, message] = layerMatch; + const [_matchStr, index, group, property, message] = layerMatch; const key = (group && property) ? [group, property].join(".") : property; return { message: error.message, @@ -396,7 +456,7 @@ export default class App extends React.Component { } }); - let dirtyMapStyle = undefined; + let dirtyMapStyle: StyleSpecification | undefined = undefined; if (errors.length > 0) { dirtyMapStyle = cloneDeep(newStyle); @@ -406,7 +466,7 @@ export default class App extends React.Component { try { const objPath = message.split(":")[0]; // Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter' - const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)[0]; + const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)![0]; unset(dirtyMapStyle, unsetPath); } catch (err) { @@ -417,17 +477,17 @@ export default class App extends React.Component { } if(newStyle.glyphs !== this.state.mapStyle.glyphs) { - this.updateFonts(newStyle.glyphs) + this.updateFonts(newStyle.glyphs as string) } if(newStyle.sprite !== this.state.mapStyle.sprite) { - this.updateIcons(newStyle.sprite) + this.updateIcons(newStyle.sprite as string) } if (opts.addRevision) { this.revisionStore.addRevision(newStyle); } if (opts.save) { - this.saveStyle(newStyle); + this.saveStyle(newStyle as StyleSpecification & {id: string}); } this.setState({ @@ -460,7 +520,7 @@ export default class App extends React.Component { }) } - onMoveLayer = (move) => { + onMoveLayer = (move: SortEnd) => { let { oldIndex, newIndex } = move; let layers = this.state.mapStyle.layers; oldIndex = clamp(oldIndex, 0, layers.length-1); @@ -478,7 +538,7 @@ export default class App extends React.Component { this.onLayersChange(layers); } - onLayersChange = (changedLayers) => { + onLayersChange = (changedLayers: LayerSpecification[]) => { const changedStyle = { ...this.state.mapStyle, layers: changedLayers @@ -486,14 +546,14 @@ export default class App extends React.Component { this.onStyleChanged(changedStyle) } - onLayerDestroy = (index) => { + onLayerDestroy = (index: number) => { let layers = this.state.mapStyle.layers; const remainingLayers = layers.slice(0); remainingLayers.splice(index, 1); this.onLayersChange(remainingLayers); } - onLayerCopy = (index) => { + onLayerCopy = (index: number) => { let layers = this.state.mapStyle.layers; const changedLayers = layers.slice(0) @@ -503,7 +563,7 @@ export default class App extends React.Component { this.onLayersChange(changedLayers) } - onLayerVisibilityToggle = (index) => { + onLayerVisibilityToggle = (index: number) => { let layers = this.state.mapStyle.layers; const changedLayers = layers.slice(0) @@ -517,7 +577,7 @@ export default class App extends React.Component { } - onLayerIdChange = (index, oldId, newId) => { + onLayerIdChange = (index: number, _oldId: string, newId: string) => { const changedLayers = this.state.mapStyle.layers.slice(0) changedLayers[index] = { ...changedLayers[index], @@ -527,26 +587,26 @@ export default class App extends React.Component { this.onLayersChange(changedLayers) } - onLayerChanged = (index, layer) => { + onLayerChanged = (index: number, layer: LayerSpecification) => { const changedLayers = this.state.mapStyle.layers.slice(0) changedLayers[index] = layer this.onLayersChange(changedLayers) } - setMapState = (newState) => { + setMapState = (newState: MapState) => { this.setState({ mapState: newState }, this.setStateInUrl); } - setDefaultValues = (styleObj) => { - const metadata = styleObj.metadata || {} + setDefaultValues = (styleObj: StyleSpecification & {id: string}) => { + const metadata: {[key: string]: string} = styleObj.metadata || {} as any if(metadata['maputnik:renderer'] === undefined) { const changedStyle = { ...styleObj, metadata: { - ...styleObj.metadata, + ...styleObj.metadata as any, 'maputnik:renderer': 'mlgljs' } } @@ -556,13 +616,13 @@ export default class App extends React.Component { } } - openStyle = (styleObj) => { + openStyle = (styleObj: StyleSpecification & {id: string}) => { styleObj = this.setDefaultValues(styleObj) this.onStyleChanged(styleObj) } fetchSources() { - const sourceList = {}; + const sourceList: {[key: string]: any} = {}; for(let [key, val] of Object.entries(this.state.mapStyle.sources)) { if( @@ -578,12 +638,12 @@ export default class App extends React.Component { let url = val.url; try { - url = setFetchAccessToken(url, this.state.mapStyle) + url = setFetchAccessToken(url!, this.state.mapStyle) } catch(err) { console.warn("Failed to setFetchAccessToken: ", err); } - fetch(url, { + fetch(url!, { mode: 'cors', }) .then(response => response.json()) @@ -599,7 +659,7 @@ export default class App extends React.Component { }); for(let layer of json.vector_layers) { - sources[key].layers.push(layer.id) + (sources[key] as any).layers.push(layer.id) } console.debug("Updating source: "+key); @@ -625,11 +685,17 @@ export default class App extends React.Component { } _getRenderer () { - const metadata = this.state.mapStyle.metadata || {}; + const metadata: {[key:string]: string} = this.state.mapStyle.metadata || {} as any; return metadata['maputnik:renderer'] || 'mlgljs'; } - onMapChange = (mapView) => { + onMapChange = (mapView: { + zoom: number, + center: { + lng: number, + lat: number, + }, + }) => { this.setState({ mapView, }); @@ -637,16 +703,15 @@ export default class App extends React.Component { mapRenderer() { const {mapStyle, dirtyMapStyle} = this.state; - const metadata = this.state.mapStyle.metadata || {}; const mapProps = { mapStyle: (dirtyMapStyle || mapStyle), - replaceAccessTokens: (mapStyle) => { + replaceAccessTokens: (mapStyle: StyleSpecification) => { return style.replaceAccessTokens(mapStyle, { allowFallback: true }); }, - onDataChange: (e) => { + onDataChange: (e: {map: Map}) => { this.layerWatcher.analyzeMap(e.map) this.fetchSources(); }, @@ -677,7 +742,7 @@ export default class App extends React.Component { if(this.state.mapState.match(/^filter-/)) { filterName = this.state.mapState.replace(/^filter-/, ""); } - const elementStyle = {}; + const elementStyle: {filter?: string} = {}; if (filterName) { elementStyle.filter = `url('#${filterName}')`; } @@ -715,12 +780,12 @@ export default class App extends React.Component { history.replaceState({selectedLayerIndex}, "Maputnik", url.href); } - getInitialStateFromUrl = (mapStyle) => { + getInitialStateFromUrl = (mapStyle: StyleSpecification) => { const url = new URL(location.href); const modalParam = url.searchParams.get("modal"); if (modalParam && modalParam !== "") { const modals = modalParam.split(","); - const modalObj = {}; + const modalObj: {[key: string]: boolean} = {}; modals.forEach(modalName => { modalObj[modalName] = true; }); @@ -735,7 +800,7 @@ export default class App extends React.Component { const view = url.searchParams.get("view"); if (view && view !== "") { - this.setMapState(view); + this.setMapState(view as MapState); } const path = url.searchParams.get("layer"); @@ -767,14 +832,14 @@ export default class App extends React.Component { } } - onLayerSelect = (index) => { + onLayerSelect = (index: number) => { this.setState({ selectedLayerIndex: index, selectedLayerOriginalId: this.state.mapStyle.layers[index].id, }, this.setStateInUrl); } - setModal(modalName, value) { + setModal(modalName: keyof AppState["isOpen"], value: boolean) { if(modalName === 'survey' && value === false) { localStorage.setItem('survey', ''); } @@ -787,11 +852,11 @@ export default class App extends React.Component { }, this.setStateInUrl) } - toggleModal(modalName) { + toggleModal(modalName: keyof AppState["isOpen"]) { this.setModal(modalName, !this.state.isOpen[modalName]); } - onChangeOpenlayersDebug = (key, value) => { + onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => { this.setState({ openlayersDebugOptions: { ...this.state.openlayersDebugOptions, @@ -800,7 +865,7 @@ export default class App extends React.Component { }); } - onChangeMaplibreGlDebug = (key, value) => { + onChangeMaplibreGlDebug = (key: keyof AppState["maplibreGlDebugOptions"], value: any) => { this.setState({ maplibreGlDebugOptions: { ...this.state.maplibreGlDebugOptions, @@ -811,8 +876,7 @@ export default class App extends React.Component { render() { const layers = this.state.mapStyle.layers || [] - const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null - const metadata = this.state.mapStyle.metadata || {} + const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined const toolbar = : null + /> : undefined const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? : null + /> : undefined const modals =
@@ -889,7 +953,6 @@ export default class App extends React.Component { onChangeMetadataProperty={this.onChangeMetadataProperty} isOpen={this.state.isOpen.settings} onOpenToggle={this.toggleModal.bind(this, 'settings')} - openlayersDebugOptions={this.state.openlayersDebugOptions} /> { } } +export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia"; + type AppToolbarProps = { mapStyle: object inspectModeEnabled: boolean @@ -108,8 +110,8 @@ type AppToolbarProps = { sources: object children?: React.ReactNode onToggleModal(...args: unknown[]): unknown - onSetMapState(...args: unknown[]): unknown - mapState?: string + onSetMapState(mapState: MapState): unknown + mapState?: MapState renderer?: string }; @@ -124,7 +126,7 @@ export default class AppToolbar extends React.Component { } } - handleSelection(val: string | undefined) { + handleSelection(val: MapState) { this.props.onSetMapState(val); } @@ -245,7 +247,7 @@ export default class AppToolbar extends React.Component { this.props.onChangeMaboxGlDebug(key, e.target.checked)} /> {key} + this.props.onChangeMaplibreGlDebug(key, e.target.checked)} /> {key} })} diff --git a/src/components/ModalExport.tsx b/src/components/ModalExport.tsx index e78782e46..2806106be 100644 --- a/src/components/ModalExport.tsx +++ b/src/components/ModalExport.tsx @@ -2,7 +2,8 @@ import React from 'react' import Slugify from 'slugify' import {saveAs} from 'file-saver' import {version} from 'maplibre-gl/package.json' -import {StyleSpecification, format} from '@maplibre/maplibre-gl-style-spec' +import {format} from '@maplibre/maplibre-gl-style-spec' +import type {StyleSpecification} from 'maplibre-gl' import {MdFileDownload} from 'react-icons/md' import FieldString from './FieldString' diff --git a/src/components/ModalSettings.tsx b/src/components/ModalSettings.tsx index 14afc108e..8df66b004 100644 --- a/src/components/ModalSettings.tsx +++ b/src/components/ModalSettings.tsx @@ -1,5 +1,6 @@ import React from 'react' -import {LightSpecification, StyleSpecification, TransitionSpecification, latest} from '@maplibre/maplibre-gl-style-spec' +import {latest} from '@maplibre/maplibre-gl-style-spec' +import type {LightSpecification, StyleSpecification, TransitionSpecification} from 'maplibre-gl' import FieldArray from './FieldArray' import FieldNumber from './FieldNumber' diff --git a/src/components/ModalSources.tsx b/src/components/ModalSources.tsx index 9669b08ef..473b7ce19 100644 --- a/src/components/ModalSources.tsx +++ b/src/components/ModalSources.tsx @@ -1,5 +1,8 @@ import React from 'react' -import {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, StyleSpecification, VectorSourceSpecification, latest} from '@maplibre/maplibre-gl-style-spec' +import {MdAddCircleOutline, MdDelete} from 'react-icons/md' +import {latest} from '@maplibre/maplibre-gl-style-spec' +import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, StyleSpecification, VectorSourceSpecification} from 'maplibre-gl' + import Modal from './Modal' import InputButton from './InputButton' import FieldString from './FieldString' @@ -10,7 +13,6 @@ import style from '../libs/style' import { deleteSource, addSource, changeSource } from '../libs/source' import publicSources from '../config/tilesets.json' -import {MdAddCircleOutline, MdDelete} from 'react-icons/md' type PublicSourceProps = { id: string diff --git a/src/components/PropertyGroup.tsx b/src/components/PropertyGroup.tsx index 1a328c775..70651c75f 100644 --- a/src/components/PropertyGroup.tsx +++ b/src/components/PropertyGroup.tsx @@ -1,7 +1,8 @@ import React from 'react' import FieldFunction from './FieldFunction' -import { LayerSpecification } from '@maplibre/maplibre-gl-style-spec' +import type {LayerSpecification} from 'maplibre-gl' + const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image'] /** Extract field spec by {@fieldName} from the {@layerType} in the @@ -39,7 +40,7 @@ type PropertyGroupProps = { groupFields: string[] onChange(...args: unknown[]): unknown spec: any - errors?: unknown[] + errors?: {[key: string]: {message: string}} }; export default class PropertyGroup extends React.Component { diff --git a/src/components/SingleFilterEditor.tsx b/src/components/SingleFilterEditor.tsx index 63661a600..1de9ddbcd 100644 --- a/src/components/SingleFilterEditor.tsx +++ b/src/components/SingleFilterEditor.tsx @@ -36,7 +36,7 @@ function parseFilter(v: string | boolean | number) { type SingleFilterEditorProps = { filter: any[] - onChange(...args: unknown[]): unknown + onChange(filter: any[]): unknown properties?: {[key: string]: string} }; diff --git a/src/components/SpecField.tsx b/src/components/SpecField.tsx index b093f59af..195842c38 100644 --- a/src/components/SpecField.tsx +++ b/src/components/SpecField.tsx @@ -13,6 +13,7 @@ const typeMap = { number: () => Block, string: () => Block, formatted: () => Block, + padding: () => Block, }; export type SpecFieldProps = InputFieldSpecProps & { diff --git a/src/components/_DataProperty.tsx b/src/components/_DataProperty.tsx index d292c6b49..7144e74f9 100644 --- a/src/components/_DataProperty.tsx +++ b/src/components/_DataProperty.tsx @@ -288,7 +288,7 @@ export default class DataProperty extends React.Component this.changeDataType(propVal)} + onChange={(propVal: string) => this.changeDataType(propVal)} title={"Select a type of data scale (default is 'categorical')."} options={this.getDataFunctionTypes(this.props.fieldSpec)} /> diff --git a/src/components/_ExpressionProperty.tsx b/src/components/_ExpressionProperty.tsx index fbafdcfd2..43dd77dc9 100644 --- a/src/components/_ExpressionProperty.tsx +++ b/src/components/_ExpressionProperty.tsx @@ -14,7 +14,7 @@ type ExpressionPropertyProps = { fieldType?: string fieldSpec?: object value?: any - errors?: {[key: string]: any} + errors?: {[key: string]: {message: string}} onChange?(...args: unknown[]): unknown onUndo?(...args: unknown[]): unknown canUndo?(...args: unknown[]): unknown @@ -109,7 +109,8 @@ export default class ExpressionProperty extends React.Component { diff --git a/src/components/_FieldMaxZoom.tsx b/src/components/_FieldMaxZoom.tsx index 58f2b09fc..2499bb853 100644 --- a/src/components/_FieldMaxZoom.tsx +++ b/src/components/_FieldMaxZoom.tsx @@ -7,7 +7,7 @@ import FieldNumber from './FieldNumber' type BlockMaxZoomProps = { value?: number onChange(...args: unknown[]): unknown - error?: unknown[] + error?: {message: string} }; export default class BlockMaxZoom extends React.Component { diff --git a/src/components/_FieldMinZoom.tsx b/src/components/_FieldMinZoom.tsx index f3edeafdd..3b48b5c87 100644 --- a/src/components/_FieldMinZoom.tsx +++ b/src/components/_FieldMinZoom.tsx @@ -7,7 +7,7 @@ import FieldNumber from './FieldNumber' type BlockMinZoomProps = { value?: number onChange(...args: unknown[]): unknown - error?: unknown[] + error?: {message: string} }; export default class BlockMinZoom extends React.Component { diff --git a/src/components/_FieldSource.tsx b/src/components/_FieldSource.tsx index c8c6c442b..0598b989f 100644 --- a/src/components/_FieldSource.tsx +++ b/src/components/_FieldSource.tsx @@ -9,7 +9,7 @@ type BlockSourceProps = { wdKey?: string onChange?(...args: unknown[]): unknown sourceIds?: unknown[] - error?: unknown[] + error?: {message: string} }; export default class BlockSource extends React.Component { diff --git a/src/components/_FieldType.tsx b/src/components/_FieldType.tsx index 8dc53009d..f23b684d8 100644 --- a/src/components/_FieldType.tsx +++ b/src/components/_FieldType.tsx @@ -9,7 +9,7 @@ type BlockTypeProps = { value: string wdKey?: string onChange(...args: unknown[]): unknown - error?: unknown[] + error?: {message: string} disabled?: boolean }; diff --git a/src/components/_SpecProperty.tsx b/src/components/_SpecProperty.tsx index bb7574b31..dc4b877e9 100644 --- a/src/components/_SpecProperty.tsx +++ b/src/components/_SpecProperty.tsx @@ -13,7 +13,7 @@ type SpecPropertyProps = SpecFieldProps & { fieldType?: string fieldSpec?: any value?: any - errors?: unknown[] + errors?: {[key: string]: {message: string}} onExpressionClick?(...args: unknown[]): unknown }; diff --git a/src/components/_ZoomProperty.tsx b/src/components/_ZoomProperty.tsx index 54be2ab6d..e14ff22f4 100644 --- a/src/components/_ZoomProperty.tsx +++ b/src/components/_ZoomProperty.tsx @@ -31,10 +31,11 @@ function setStopRefs(props: ZoomPropertyProps, state: ZoomPropertyState) { newRefs = {...state}; } newRefs[idx] = docUid("stop-"); + } else { + newRefs[idx] = state.refs[idx]; } }) } - return newRefs; } @@ -156,7 +157,6 @@ export default class ZoomProperty extends React.Component - return @@ -195,7 +195,7 @@ export default class ZoomProperty extends React.Component this.changeDataType(propVal)} + onChange={(propVal: string) => this.changeDataType(propVal)} title={"Select a type of data scale (default is 'categorical')."} options={this.getDataFunctionTypes(this.props.fieldSpec!)} /> diff --git a/src/libs/apistore.ts b/src/libs/apistore.ts index 59f30d984..d403fb741 100644 --- a/src/libs/apistore.ts +++ b/src/libs/apistore.ts @@ -1,10 +1,11 @@ import style from './style.js' -import {StyleSpecification, format} from '@maplibre/maplibre-gl-style-spec' +import {format} from '@maplibre/maplibre-gl-style-spec' +import type {StyleSpecification} from 'maplibre-gl' import ReconnectingWebSocket from 'reconnecting-websocket' export type ApiStyleStoreOptions = { - port?: string - host?: string + port: string | null + host: string | null onLocalStyleChange?: (style: any) => void } diff --git a/src/libs/diffmessage.ts b/src/libs/diffmessage.ts index 95cfe0bcc..b4fe90774 100644 --- a/src/libs/diffmessage.ts +++ b/src/libs/diffmessage.ts @@ -1,4 +1,5 @@ -import {StyleSpecification, diff} from '@maplibre/maplibre-gl-style-spec' +import {diff} from '@maplibre/maplibre-gl-style-spec' +import type {StyleSpecification} from 'maplibre-gl' function diffMessages(beforeStyle: StyleSpecification, afterStyle: StyleSpecification) { const changes = diff(beforeStyle, afterStyle) diff --git a/src/libs/highlight.ts b/src/libs/highlight.ts index 6f4962ba3..e6fe56869 100644 --- a/src/libs/highlight.ts +++ b/src/libs/highlight.ts @@ -2,40 +2,42 @@ import stylegen from 'mapbox-gl-inspect/lib/stylegen' // @ts-ignore import colors from 'mapbox-gl-inspect/lib/colors' -import {FilterSpecification,LayerSpecification } from '@maplibre/maplibre-gl-style-spec' +import type {FilterSpecification,LayerSpecification } from 'maplibre-gl' -export function colorHighlightedLayer(layer: LayerSpecification) { - if(!layer || layer.type === 'background' || layer.type === 'raster') return null +export type HighlightedLayer = LayerSpecification & {filter?: FilterSpecification}; + +function changeLayer(l: HighlightedLayer, layer: LayerSpecification) { + if(l.type === 'circle') { + l.paint!['circle-radius'] = 3 + } else if(l.type === 'line') { + l.paint!['line-width'] = 2 + } - function changeLayer(l: LayerSpecification & {filter?: FilterSpecification}) { - if(l.type === 'circle') { - l.paint!['circle-radius'] = 3 - } else if(l.type === 'line') { - l.paint!['line-width'] = 2 - } - - if("filter" in layer) { - l.filter = layer.filter - } else { - delete l['filter'] - } - l.id = l.id + '_highlight' - return l + if("filter" in layer) { + l.filter = layer.filter + } else { + delete l['filter'] } + l.id = l.id + '_highlight' + return l +} + +export function colorHighlightedLayer(layer?: LayerSpecification): HighlightedLayer | null { + if(!layer || layer.type === 'background' || layer.type === 'raster') return null const sourceLayerId = layer['source-layer'] || '' const color = colors.brightColor(sourceLayerId, 1); if(layer.type === "fill" || layer.type === 'fill-extrusion') { - return changeLayer(stylegen.polygonLayer(color, color, layer.source, layer['source-layer'])) + return changeLayer(stylegen.polygonLayer(color, color, layer.source, layer['source-layer']), layer) } if(layer.type === "symbol" || layer.type === 'circle') { - return changeLayer(stylegen.circleLayer(color, layer.source, layer['source-layer'])) + return changeLayer(stylegen.circleLayer(color, layer.source, layer['source-layer']), layer) } if(layer.type === 'line') { - return changeLayer(stylegen.lineLayer(color, layer.source, layer['source-layer'])) + return changeLayer(stylegen.lineLayer(color, layer.source, layer['source-layer']), layer) } return null diff --git a/src/libs/layer.ts b/src/libs/layer.ts index b7b0f0c71..38b27d88b 100644 --- a/src/libs/layer.ts +++ b/src/libs/layer.ts @@ -27,7 +27,7 @@ export function changeType(layer: LayerSpecification, newType: string) { /** A {@property} in either the paint our layout {@group} has changed * to a {@newValue}. */ -export function changeProperty(layer: LayerSpecification, group: keyof LayerSpecification, property: string, newValue: any) { +export function changeProperty(layer: LayerSpecification, group: keyof LayerSpecification | null, property: string, newValue: any) { // Remove the property if undefined if(newValue === undefined) { if(group) { diff --git a/src/libs/revisions.ts b/src/libs/revisions.ts index 4caa5de62..4cdf0f4c4 100644 --- a/src/libs/revisions.ts +++ b/src/libs/revisions.ts @@ -1,7 +1,7 @@ -import type {StyleSpecification} from "@maplibre/maplibre-gl-style-spec"; +import type {StyleSpecification} from "maplibre-gl"; export class RevisionStore { - revisions: StyleSpecification[]; + revisions: (StyleSpecification & {id: string})[]; currentIdx: number; @@ -18,7 +18,7 @@ export class RevisionStore { return this.revisions[this.currentIdx] } - addRevision(revision: StyleSpecification) { + addRevision(revision: StyleSpecification & {id: string}) { //TODO: compare new revision style id with old ones //and ensure that it is always the same id this.revisions.push(revision) diff --git a/src/libs/source.ts b/src/libs/source.ts index f3fa1f856..80487902c 100644 --- a/src/libs/source.ts +++ b/src/libs/source.ts @@ -1,4 +1,4 @@ -import type {StyleSpecification, SourceSpecification} from "@maplibre/maplibre-gl-style-spec"; +import type {StyleSpecification, SourceSpecification} from "maplibre-gl"; export function deleteSource(mapStyle: StyleSpecification, sourceId: string) { const remainingSources = { ...mapStyle.sources} diff --git a/src/libs/style.ts b/src/libs/style.ts index 016140d08..1c71ae931 100644 --- a/src/libs/style.ts +++ b/src/libs/style.ts @@ -1,4 +1,5 @@ -import {derefLayers, StyleSpecification, LayerSpecification} from '@maplibre/maplibre-gl-style-spec' +import {derefLayers} from '@maplibre/maplibre-gl-style-spec' +import type {StyleSpecification, LayerSpecification} from 'maplibre-gl' import tokens from '../config/tokens.json' // Empty style is always used if no style could be restored or fetched diff --git a/src/libs/stylestore.ts b/src/libs/stylestore.ts index 71138d3d0..506c5636c 100644 --- a/src/libs/stylestore.ts +++ b/src/libs/stylestore.ts @@ -1,7 +1,7 @@ import style from './style' import {loadStyleUrl} from './urlopen' import publicSources from '../config/styles.json' -import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec' +import type {StyleSpecification} from 'maplibre-gl' const storagePrefix = "maputnik" const stylePrefix = 'style' diff --git a/src/util/codemirror-mgl.js b/src/util/codemirror-mgl.ts similarity index 81% rename from src/util/codemirror-mgl.js rename to src/util/codemirror-mgl.ts index f285b5b2c..84219cf46 100644 --- a/src/util/codemirror-mgl.js +++ b/src/util/codemirror-mgl.ts @@ -1,24 +1,27 @@ +// @ts-ignore - this is a fork of jsonlint import jsonlint from 'jsonlint'; -import CodeMirror from 'codemirror'; +import CodeMirror, { MarkerRange } from 'codemirror'; import jsonToAst from 'json-to-ast'; import {expression, validate} from '@maplibre/maplibre-gl-style-spec'; +type MarkerRangeWithMessage = MarkerRange & {message: string}; -CodeMirror.defineMode("mgl", function(config, parserConfig) { + +CodeMirror.defineMode("mgl", (config, parserConfig) => { // Just using the javascript mode with json enabled. Our logic is in the linter below. return CodeMirror.modes.javascript( - {...config, json: true}, + {...config, json: true} as any, parserConfig ); }); -CodeMirror.registerHelper("lint", "json", function(text) { - const found = []; +CodeMirror.registerHelper("lint", "json", (text: string) => { + const found: MarkerRangeWithMessage[] = []; // NOTE: This was modified from the original to remove the global, also the // old jsonlint API was 'jsonlint.parseError' its now // 'jsonlint.parser.parseError' - jsonlint.parser.parseError = function(str, hash) { + (jsonlint as any).parser.parseError = (str: string, hash: any) => { const loc = hash.loc; found.push({ from: CodeMirror.Pos(loc.first_line - 1, loc.first_column), @@ -36,12 +39,12 @@ CodeMirror.registerHelper("lint", "json", function(text) { return found; }); -CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) { - const found = []; - const {parser} = jsonlint; +CodeMirror.registerHelper("lint", "mgl", (text: string, opts: any, doc: any) => { + const found: MarkerRangeWithMessage[] = []; + const {parser} = jsonlint as any; const {context} = opts; - parser.parseError = function(str, hash) { + parser.parseError = (str: string, hash: any) => { const loc = hash.loc; found.push({ from: CodeMirror.Pos(loc.first_line - 1, loc.first_column), @@ -62,7 +65,7 @@ CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) { const ast = jsonToAst(text); const input = JSON.parse(text); - function getArrayPositionalFromAst (node, path) { + function getArrayPositionalFromAst(node: any, path: string[]) { if (!node) { return undefined; } @@ -79,7 +82,7 @@ CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) { newNode = node.children[path[0]]; } else { - newNode = node.children.find(childNode => { + newNode = node.children.find((childNode: any) => { return ( childNode.key && childNode.key.type === "Identifier" && @@ -94,7 +97,7 @@ CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) { } } - let out; + let out: ReturnType | null = null; if (context === "layer") { // Just an empty style so we can validate a layer. const errors = validate({ @@ -121,6 +124,7 @@ CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) { // Remove the 'layers[0].' as we're validating the layer only here const errMessageParts = err.message.replace(/^layers\[0\]./, "").split(":"); return { + name: '', key: errMessageParts[0], message: errMessageParts[1], }; @@ -135,7 +139,7 @@ CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) { throw new Error(`Invalid context ${context}`); } - if (out.result === "error") { + if (out?.result === "error") { const errors = out.value; errors.forEach(error => { const {key, message} = error;