diff --git a/package-lock.json b/package-lock.json index 8c039f7ca..3eada999f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,6 +68,7 @@ "@storybook/react-vite": "^7.6.5", "@storybook/theming": "^7.6.5", "@types/cors": "^2.8.17", + "@types/lodash.isequal": "^4.5.8", "@types/lodash.throttle": "^4.1.9", "@types/react": "^16.14.52", "@types/react-dom": "^16.9.24", @@ -4702,6 +4703,15 @@ "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", "dev": true }, + "node_modules/@types/lodash.isequal": { + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz", + "integrity": "sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/lodash.throttle": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz", diff --git a/package.json b/package.json index 7b34cc197..75b11d12b 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "@storybook/react-vite": "^7.6.5", "@storybook/theming": "^7.6.5", "@types/cors": "^2.8.17", + "@types/lodash.isequal": "^4.5.8", "@types/lodash.throttle": "^4.1.9", "@types/react": "^16.14.52", "@types/react-dom": "^16.9.24", diff --git a/src/components/ModalOpen.jsx b/src/components/ModalOpen.jsx index f399fdc1f..6b542941e 100644 --- a/src/components/ModalOpen.jsx +++ b/src/components/ModalOpen.jsx @@ -9,7 +9,7 @@ import InputUrl from './InputUrl' import {MdFileUpload} from 'react-icons/md' import {MdAddCircleOutline} from 'react-icons/md' -import style from '../libs/style.js' +import style from '../libs/style' import publicStyles from '../config/styles.json' class PublicStyle extends React.Component { diff --git a/src/libs/apistore.js b/src/libs/apistore.ts similarity index 79% rename from src/libs/apistore.js rename to src/libs/apistore.ts index 3432fb501..59f30d984 100644 --- a/src/libs/apistore.js +++ b/src/libs/apistore.ts @@ -1,10 +1,21 @@ import style from './style.js' -import {format} from '@maplibre/maplibre-gl-style-spec' +import {StyleSpecification, format} from '@maplibre/maplibre-gl-style-spec' import ReconnectingWebSocket from 'reconnecting-websocket' +export type ApiStyleStoreOptions = { + port?: string + host?: string + onLocalStyleChange?: (style: any) => void +} + export class ApiStyleStore { - constructor(opts) { + localUrl: string; + websocketUrl: string; + latestStyleId: string | undefined = undefined; + onLocalStyleChange: (style: any) => void; + + constructor(opts: ApiStyleStoreOptions) { this.onLocalStyleChange = opts.onLocalStyleChange || (() => {}) const port = opts.port || '8000' const host = opts.host || 'localhost' @@ -13,7 +24,7 @@ export class ApiStyleStore { this.init = this.init.bind(this) } - init(cb) { + init(cb: (...args: any[]) => void) { fetch(this.localUrl + '/styles', { mode: 'cors', }) @@ -26,7 +37,7 @@ export class ApiStyleStore { this.notifyLocalChanges() cb(null) }) - .catch(function(e) { + .catch(() => { cb(new Error('Can not connect to style API')) }) } @@ -47,7 +58,7 @@ export class ApiStyleStore { } } - latestStyle(cb) { + latestStyle(cb: (...args: any[]) => void) { if(this.latestStyleId) { fetch(this.localUrl + '/styles/' + this.latestStyleId, { mode: 'cors', @@ -64,7 +75,7 @@ export class ApiStyleStore { } // Save current style replacing previous version - save(mapStyle) { + save(mapStyle: StyleSpecification & { id: string }) { const styleJSON = format( style.stripAccessTokens( style.replaceAccessTokens(mapStyle) diff --git a/src/libs/field-spec-additional.js b/src/libs/field-spec-additional.ts similarity index 100% rename from src/libs/field-spec-additional.js rename to src/libs/field-spec-additional.ts diff --git a/src/libs/highlight.js b/src/libs/highlight.ts similarity index 68% rename from src/libs/highlight.js rename to src/libs/highlight.ts index 6390dcd2a..6f4962ba3 100644 --- a/src/libs/highlight.js +++ b/src/libs/highlight.ts @@ -1,17 +1,20 @@ +// @ts-ignore 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' -export function colorHighlightedLayer(layer) { +export function colorHighlightedLayer(layer: LayerSpecification) { if(!layer || layer.type === 'background' || layer.type === 'raster') return null - function changeLayer(l) { + function changeLayer(l: LayerSpecification & {filter?: FilterSpecification}) { if(l.type === 'circle') { - l.paint['circle-radius'] = 3 + l.paint!['circle-radius'] = 3 } else if(l.type === 'line') { - l.paint['line-width'] = 2 + l.paint!['line-width'] = 2 } - if(layer.filter) { + if("filter" in layer) { l.filter = layer.filter } else { delete l['filter'] @@ -21,8 +24,7 @@ export function colorHighlightedLayer(layer) { } const sourceLayerId = layer['source-layer'] || '' - const color = colors.brightColor(sourceLayerId, 1) - const layers = [] + 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'])) diff --git a/src/libs/layer.js b/src/libs/layer.ts similarity index 64% rename from src/libs/layer.js rename to src/libs/layer.ts index 0fad7f3b1..b7b0f0c71 100644 --- a/src/libs/layer.js +++ b/src/libs/layer.ts @@ -1,17 +1,18 @@ import {latest} from '@maplibre/maplibre-gl-style-spec' +import { LayerSpecification } from 'maplibre-gl' -export function changeType(layer, newType) { - const changedPaintProps = { ...layer.paint } +export function changeType(layer: LayerSpecification, newType: string) { + const changedPaintProps: LayerSpecification["paint"] = { ...layer.paint } Object.keys(changedPaintProps).forEach(propertyName => { if(!(propertyName in latest['paint_' + newType])) { - delete changedPaintProps[propertyName] + delete changedPaintProps[propertyName as keyof LayerSpecification["paint"]] } }) - const changedLayoutProps = { ...layer.layout } + const changedLayoutProps: LayerSpecification["layout"] = { ...layer.layout } Object.keys(changedLayoutProps).forEach(propertyName => { if(!(propertyName in latest['layout_' + newType])) { - delete changedLayoutProps[propertyName] + delete changedLayoutProps[propertyName as keyof LayerSpecification["layout"]] } }) @@ -26,15 +27,15 @@ export function changeType(layer, newType) { /** A {@property} in either the paint our layout {@group} has changed * to a {@newValue}. */ -export function changeProperty(layer, group, property, newValue) { +export function changeProperty(layer: LayerSpecification, group: keyof LayerSpecification, property: string, newValue: any) { // Remove the property if undefined if(newValue === undefined) { if(group) { - const newLayer = { + const newLayer: any = { ...layer, // Change object so the diff works in ./src/components/map/MaplibreGlMap.jsx [group]: { - ...layer[group] + ...layer[group] as any } }; delete newLayer[group][property]; @@ -45,7 +46,7 @@ export function changeProperty(layer, group, property, newValue) { } return newLayer; } else { - const newLayer = { + const newLayer: any = { ...layer }; delete newLayer[property]; @@ -57,7 +58,7 @@ export function changeProperty(layer, group, property, newValue) { return { ...layer, [group]: { - ...layer[group], + ...layer[group] as any, [property]: newValue } } diff --git a/src/libs/layerwatcher.js b/src/libs/layerwatcher.ts similarity index 69% rename from src/libs/layerwatcher.js rename to src/libs/layerwatcher.ts index ad117ac5d..c0fb1f30e 100644 --- a/src/libs/layerwatcher.js +++ b/src/libs/layerwatcher.ts @@ -1,10 +1,22 @@ import throttle from 'lodash.throttle' import isEqual from 'lodash.isequal' +import { Map } from 'maplibre-gl'; + +export type LayerWatcherOptions = { + onSourcesChange?: (sources: { [sourceId: string]: string[] }) => void; + onVectorLayersChange?: (vectorLayers: { [vectorLayerId: string]: { [propertyName: string]: { [propertyValue: string]: {} } } }) => void; +} /** Listens to map events to build up a store of available vector * layers contained in the tiles */ export default class LayerWatcher { - constructor(opts = {}) { + onSourcesChange: (sources: { [sourceId: string]: string[] }) => void; + onVectorLayersChange: (vectorLayers: { [vectorLayerId: string]: { [propertyName: string]: { [propertyValue: string]: {} } } }) => void; + throttledAnalyzeVectorLayerFields: (map: any) => void; + _sources: { [sourceId: string]: string[] }; + _vectorLayers: { [vectorLayerId: string]: { [propertyName: string]: { [propertyValue: string]: {} } } }; + + constructor(opts: LayerWatcherOptions = {}) { this.onSourcesChange = opts.onSourcesChange || (() => {}) this.onVectorLayersChange = opts.onVectorLayersChange || (() => {}) @@ -17,13 +29,13 @@ export default class LayerWatcher { this.throttledAnalyzeVectorLayerFields = throttle(this.analyzeVectorLayerFields, 5000) } - analyzeMap(map) { + analyzeMap(map: Map) { const previousSources = { ...this._sources } Object.keys(map.style.sourceCaches).forEach(sourceId => { //NOTE: This heavily depends on the internal API of Maplibre GL //so this breaks between Maplibre GL JS releases - this._sources[sourceId] = map.style.sourceCaches[sourceId]._source.vectorLayerIds + this._sources[sourceId] = map.style.sourceCaches[sourceId]._source.vectorLayerIds as string[]; }) if(!isEqual(previousSources, this._sources)) { @@ -33,14 +45,14 @@ export default class LayerWatcher { this.throttledAnalyzeVectorLayerFields(map) } - analyzeVectorLayerFields(map) { + analyzeVectorLayerFields(map: Map) { const previousVectorLayers = { ...this._vectorLayers } Object.keys(this._sources).forEach(sourceId => { (this._sources[sourceId] || []).forEach(vectorLayerId => { const knownProperties = this._vectorLayers[vectorLayerId] || {} const params = { sourceLayer: vectorLayerId } - map.querySourceFeatures(sourceId, params).forEach(feature => { + map.querySourceFeatures(sourceId, params as any).forEach(feature => { Object.keys(feature.properties).forEach(propertyName => { const knownPropertyValues = knownProperties[propertyName] || {} knownPropertyValues[feature.properties[propertyName]] = {} diff --git a/src/libs/maplibre-rtl.js b/src/libs/maplibre-rtl.ts similarity index 64% rename from src/libs/maplibre-rtl.js rename to src/libs/maplibre-rtl.ts index c2b0082fe..7eb16e1a5 100644 --- a/src/libs/maplibre-rtl.js +++ b/src/libs/maplibre-rtl.ts @@ -1,3 +1,3 @@ import MapLibreGl from "maplibre-gl" -MapLibreGl.setRTLTextPlugin('https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.min.js'); +MapLibreGl.setRTLTextPlugin('https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.min.js', () => {}); diff --git a/src/libs/metadata.js b/src/libs/metadata.ts similarity index 72% rename from src/libs/metadata.js rename to src/libs/metadata.ts index 2e1fb243f..f0474a56f 100644 --- a/src/libs/metadata.js +++ b/src/libs/metadata.ts @@ -1,6 +1,6 @@ import npmurl from 'url' -function loadJSON(url, defaultValue, cb) { +function loadJSON(url: string, defaultValue: any, cb: (...args: any[]) => void) { fetch(url, { mode: 'cors', credentials: "same-origin" @@ -17,7 +17,7 @@ function loadJSON(url, defaultValue, cb) { }) } -export function downloadGlyphsMetadata(urlTemplate, cb) { +export function downloadGlyphsMetadata(urlTemplate: string, cb: (...args: any[]) => void) { if(!urlTemplate) return cb([]) // Special handling because Tileserver GL serves the fontstacks metadata differently @@ -27,14 +27,14 @@ export function downloadGlyphsMetadata(urlTemplate, cb) { if(urlObj.pathname === normPathPart) { urlObj.pathname = '/fontstacks.json'; } else { - urlObj.pathname = urlObj.pathname.replace(normPathPart, '.json'); + urlObj.pathname = urlObj.pathname!.replace(normPathPart, '.json'); } let url = npmurl.format(urlObj); loadJSON(url, [], cb) } -export function downloadSpriteMetadata(baseUrl, cb) { +export function downloadSpriteMetadata(baseUrl: string, cb: (...args: any[]) => void) { if(!baseUrl) return cb([]) const url = baseUrl + '.json' loadJSON(url, {}, glyphs => cb(Object.keys(glyphs))) diff --git a/src/libs/revisions.js b/src/libs/revisions.ts similarity index 70% rename from src/libs/revisions.js rename to src/libs/revisions.ts index 51abcf00c..4caa5de62 100644 --- a/src/libs/revisions.js +++ b/src/libs/revisions.ts @@ -1,4 +1,10 @@ +import type {StyleSpecification} from "@maplibre/maplibre-gl-style-spec"; + export class RevisionStore { + revisions: StyleSpecification[]; + currentIdx: number; + + constructor(initialRevisions=[]) { this.revisions = initialRevisions this.currentIdx = initialRevisions.length - 1 @@ -12,7 +18,7 @@ export class RevisionStore { return this.revisions[this.currentIdx] } - addRevision(revision) { + addRevision(revision: StyleSpecification) { //TODO: compare new revision style id with old ones //and ensure that it is always the same id this.revisions.push(revision) @@ -21,15 +27,15 @@ export class RevisionStore { undo() { if(this.currentIdx > 0) { - this.currentIdx-- + this.currentIdx--; } - return this.current + return this.current; } redo() { if(this.currentIdx < this.revisions.length - 1) { this.currentIdx++ } - return this.current + return this.current; } } diff --git a/src/libs/source.js b/src/libs/source.js deleted file mode 100644 index ed8693d63..000000000 --- a/src/libs/source.js +++ /dev/null @@ -1,25 +0,0 @@ -export function deleteSource(mapStyle, sourceId) { - const remainingSources = { ...mapStyle.sources} - delete remainingSources[sourceId] - return { - ...mapStyle, - sources: remainingSources - } -} - - -export function addSource(mapStyle, sourceId, source) { - return changeSource(mapStyle, sourceId, source) -} - -export function changeSource(mapStyle, sourceId, source) { - const changedSources = { - ...mapStyle.sources, - [sourceId]: source - } - return { - ...mapStyle, - sources: changedSources - } -} - diff --git a/src/libs/source.ts b/src/libs/source.ts new file mode 100644 index 000000000..f3fa1f856 --- /dev/null +++ b/src/libs/source.ts @@ -0,0 +1,27 @@ +import type {StyleSpecification, SourceSpecification} from "@maplibre/maplibre-gl-style-spec"; + +export function deleteSource(mapStyle: StyleSpecification, sourceId: string) { + const remainingSources = { ...mapStyle.sources} + delete remainingSources[sourceId] + return { + ...mapStyle, + sources: remainingSources + } +} + + +export function addSource(mapStyle: StyleSpecification, sourceId: string, source: SourceSpecification) { + return changeSource(mapStyle, sourceId, source) +} + +export function changeSource(mapStyle: StyleSpecification, sourceId: string, source: SourceSpecification) { + const changedSources = { + ...mapStyle.sources, + [sourceId]: source + } + return { + ...mapStyle, + sources: changedSources + } +} + diff --git a/src/libs/style.js b/src/libs/style.ts similarity index 62% rename from src/libs/style.js rename to src/libs/style.ts index 4eb90b68a..016140d08 100644 --- a/src/libs/style.js +++ b/src/libs/style.ts @@ -1,4 +1,4 @@ -import {derefLayers} from '@maplibre/maplibre-gl-style-spec' +import {derefLayers, StyleSpecification, LayerSpecification} from '@maplibre/maplibre-gl-style-spec' import tokens from '../config/tokens.json' // Empty style is always used if no style could be restored or fetched @@ -9,18 +9,20 @@ const emptyStyle = ensureStyleValidity({ }) function generateId() { - return Math.random().toString(36).substr(2, 9) + return Math.random().toString(36).substring(2, 9) } -function ensureHasId(style) { - if('id' in style) return style - style.id = generateId() - return style +function ensureHasId(style: StyleSpecification & { id?: string }): StyleSpecification & { id: string } { + if(!('id' in style) || !style.id) { + style.id = generateId(); + return style as StyleSpecification & { id: string }; + } + return style as StyleSpecification & { id: string }; } -function ensureHasNoInteractive(style) { +function ensureHasNoInteractive(style: StyleSpecification & {id: string}) { const changedLayers = style.layers.map(layer => { - const changedLayer = { ...layer } + const changedLayer: LayerSpecification & { interactive?: any } = { ...layer } delete changedLayer.interactive return changedLayer }) @@ -31,18 +33,18 @@ function ensureHasNoInteractive(style) { } } -function ensureHasNoRefs(style) { +function ensureHasNoRefs(style: StyleSpecification & {id: string}) { return { ...style, layers: derefLayers(style.layers) } } -function ensureStyleValidity(style) { +function ensureStyleValidity(style: StyleSpecification): StyleSpecification & { id: string } { return ensureHasNoInteractive(ensureHasNoRefs(ensureHasId(style))) } -function indexOfLayer(layers, layerId) { +function indexOfLayer(layers: LayerSpecification[], layerId: string) { for (let i = 0; i < layers.length; i++) { if(layers[i].id === layerId) { return i @@ -51,25 +53,25 @@ function indexOfLayer(layers, layerId) { return null } -function getAccessToken(sourceName, mapStyle, opts) { +function getAccessToken(sourceName: string, mapStyle: StyleSpecification, opts: {allowFallback?: boolean}) { if(sourceName === "thunderforest_transport" || sourceName === "thunderforest_outdoors") { sourceName = "thunderforest" } - const metadata = mapStyle.metadata || {} + const metadata = mapStyle.metadata || {} as any; let accessToken = metadata[`maputnik:${sourceName}_access_token`] if(opts.allowFallback && !accessToken) { - accessToken = tokens[sourceName] + accessToken = tokens[sourceName as keyof typeof tokens] } return accessToken; } -function replaceSourceAccessToken(mapStyle, sourceName, opts={}) { +function replaceSourceAccessToken(mapStyle: StyleSpecification, sourceName: string, opts={}) { const source = mapStyle.sources[sourceName] if(!source) return mapStyle - if(!source.hasOwnProperty("url")) return mapStyle + if(!("url" in source) || !source.url) return mapStyle const accessToken = getAccessToken(sourceName, mapStyle, opts) @@ -92,7 +94,7 @@ function replaceSourceAccessToken(mapStyle, sourceName, opts={}) { return changedStyle } -function replaceAccessTokens(mapStyle, opts={}) { +function replaceAccessTokens(mapStyle: StyleSpecification, opts={}) { let changedStyle = mapStyle Object.keys(mapStyle.sources).forEach((sourceName) => { @@ -112,9 +114,9 @@ function replaceAccessTokens(mapStyle, opts={}) { return changedStyle } -function stripAccessTokens(mapStyle) { +function stripAccessTokens(mapStyle: StyleSpecification) { const changedMetadata = { - ...mapStyle.metadata + ...mapStyle.metadata as any }; delete changedMetadata['maputnik:openmaptiles_access_token']; return { diff --git a/src/libs/stylestore.js b/src/libs/stylestore.ts similarity index 75% rename from src/libs/stylestore.js rename to src/libs/stylestore.ts index 5a6e69a26..71138d3d0 100644 --- a/src/libs/stylestore.js +++ b/src/libs/stylestore.ts @@ -1,6 +1,7 @@ -import style from './style.js' -import { loadStyleUrl } from './urlopen' +import style from './style' +import {loadStyleUrl} from './urlopen' import publicSources from '../config/styles.json' +import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec' const storagePrefix = "maputnik" const stylePrefix = 'style' @@ -12,7 +13,7 @@ const storageKeys = { const defaultStyleUrl = publicSources[0].url // Fetch a default style via URL and return it or a fallback style via callback -export function loadDefaultStyle(cb) { +export function loadDefaultStyle(cb: (...args: any[]) => void) { loadStyleUrl(defaultStyleUrl, cb) } @@ -21,20 +22,20 @@ function loadStoredStyles() { const styles = [] for (let i = 0; i < window.localStorage.length; i++) { const key = window.localStorage.key(i) - if(isStyleKey(key)) { - styles.push(fromKey(key)) + if(isStyleKey(key!)) { + styles.push(fromKey(key!)) } } return styles } -function isStyleKey(key) { +function isStyleKey(key: string) { const parts = key.split(":") return parts.length === 3 && parts[0] === storagePrefix && parts[1] === stylePrefix } // Load style id from key -function fromKey(key) { +function fromKey(key: string) { if(!isStyleKey(key)) { throw "Key is not a valid style key" } @@ -45,26 +46,31 @@ function fromKey(key) { } // Calculate key that identifies the style with a version -function styleKey(styleId) { +function styleKey(styleId: string) { return [storagePrefix, stylePrefix, styleId].join(":") } // Manages many possible styles that are stored in the local storage export class StyleStore { + /** + * List of style ids + */ + mapStyles: string[]; + // Tile store will load all items from local storage and // assume they do not change will working on it constructor() { - this.mapStyles = loadStoredStyles() + this.mapStyles = loadStoredStyles(); } - init(cb) { + init(cb: (...args: any[]) => void) { cb(null) } // Delete entire style history purge() { for (let i = 0; i < window.localStorage.length; i++) { - const key = window.localStorage.key(i) + const key = window.localStorage.key(i) as string; if(key.startsWith(storagePrefix)) { window.localStorage.removeItem(key) } @@ -72,9 +78,9 @@ export class StyleStore { } // Find the last edited style - latestStyle(cb) { + latestStyle(cb: (...args: any[]) => void) { if(this.mapStyles.length === 0) return loadDefaultStyle(cb) - const styleId = window.localStorage.getItem(storageKeys.latest) + const styleId = window.localStorage.getItem(storageKeys.latest) as string; const styleItem = window.localStorage.getItem(styleKey(styleId)) if(styleItem) return cb(JSON.parse(styleItem)) @@ -82,7 +88,7 @@ export class StyleStore { } // Save current style replacing previous version - save(mapStyle) { + save(mapStyle: StyleSpecification & { id: string }) { mapStyle = style.ensureStyleValidity(mapStyle) const key = styleKey(mapStyle.id) window.localStorage.setItem(key, JSON.stringify(mapStyle))