From abe6230932d862a57720a83ec71b600b1611659a Mon Sep 17 00:00:00 2001 From: Harel M Date: Mon, 8 Sep 2025 15:22:29 +0300 Subject: [PATCH] Move style and store initialization to mount method (#1351) This is in order to reduce warnings in the console for React 19 usage. This removes the deprecated defaultProp and also move all the store initialization logic out of the App.tsx file, keeping it a lot more clean. It removes the `debug` flag from the supported urls along with the `localport` and `localhost`, which I'm not sure if and how they were ever used. The tests are using the `style` url, so I think it is covered in terms of tests. It also improves some typings along the project. It removes some callbacks from the code and moves to use promises. ## Launch Checklist - [x] Briefly describe the changes in this PR. - [x] Include before/after visuals or gifs if this PR includes visual changes. - [x] Write tests for all new functionality. - [ ] Add an entry to `CHANGELOG.md` under the `## main` section. Before: image After: image --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bart Louwers --- CHANGELOG.md | 1 + cypress/e2e/history.cy.ts | 1 - cypress/e2e/maputnik-driver.ts | 14 +-- package.json | 4 +- src/components/App.tsx | 150 +++++++++----------------- src/components/AppToolbar.tsx | 5 +- src/components/FieldType.tsx | 30 +++--- src/components/FilterEditor.tsx | 5 +- src/components/ModalExport.tsx | 6 +- src/components/ModalSettings.tsx | 7 +- src/components/ModalSources.tsx | 7 +- src/libs/debug.ts | 48 --------- src/libs/definitions.d.ts | 16 +++ src/libs/revisions.ts | 6 +- src/libs/source.ts | 9 +- src/libs/{ => store}/apistore.ts | 58 ++++------ src/libs/store/style-store-factory.ts | 29 +++++ src/libs/{ => store}/stylestore.ts | 32 +++--- src/libs/style.ts | 12 +-- src/libs/urlopen.ts | 43 ++++---- vite.config.ts | 7 +- 21 files changed, 218 insertions(+), 272 deletions(-) delete mode 100644 src/libs/debug.ts create mode 100644 src/libs/definitions.d.ts rename src/libs/{ => store}/apistore.ts (60%) create mode 100644 src/libs/store/style-store-factory.ts rename src/libs/{ => store}/stylestore.ts (79%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7d9b607..5d74ccd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Refactor Field components to use arrow function syntax - Replace react-autocomplete with Downshift in the autocomplete component - Add LocationIQ as supported map provider with access token field and gallery style +- Revmove support for `debug` and `localport` url parameters - _...Add new stuff here..._ ### 🐞 Bug fixes diff --git a/cypress/e2e/history.cy.ts b/cypress/e2e/history.cy.ts index 3934ee61..499a09b2 100644 --- a/cypress/e2e/history.cy.ts +++ b/cypress/e2e/history.cy.ts @@ -16,7 +16,6 @@ describe("history", () => { it("undo/redo", () => { when.setStyle("geojson"); when.modal.open(); - then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] }); when.modal.fillLayers({ id: "step 1", diff --git a/cypress/e2e/maputnik-driver.ts b/cypress/e2e/maputnik-driver.ts index 436fa55c..5af27794 100644 --- a/cypress/e2e/maputnik-driver.ts +++ b/cypress/e2e/maputnik-driver.ts @@ -110,25 +110,25 @@ export class MaputnikDriver { styleProperties: "geojson" | "raster" | "both" | "layer" | "", zoom?: number ) => { - let url = "?debug"; + const url = new URL(baseUrl); switch (styleProperties) { case "geojson": - url += `&style=${baseUrl}geojson-style.json`; + url.searchParams.set("style", baseUrl + "geojson-style.json"); break; case "raster": - url += `&style=${baseUrl}raster-style.json`; + url.searchParams.set("style", baseUrl + "raster-style.json"); break; case "both": - url += `&style=${baseUrl}geojson-raster-style.json`; + url.searchParams.set("style", baseUrl + "geojson-raster-style.json"); break; case "layer": - url += `&style=${baseUrl}/example-layer-style.json`; + url.searchParams.set("style", baseUrl + "example-layer-style.json"); break; } if (zoom) { - url += `#${zoom}/41.3805/2.1635`; + url.hash = `${zoom}/41.3805/2.1635`; } - this.helper.when.visit(baseUrl + url); + this.helper.when.visit(url.toString()); if (styleProperties) { this.helper.when.acceptConfirm(); } diff --git a/package.json b/package.json index 4f56899b..b12d4fab 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "main": "''", "scripts": { "start": "vite", - "build": "tsc && vite build --base=/maputnik/", - "build-desktop": "tsc && vite build --base=/ && cd desktop && make", + "build": "tsc && vite build --mode=production", + "build-desktop": "tsc && vite build --mode=desktop && cd desktop && make", "i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'", "lint": "eslint", "test": "cypress run", diff --git a/src/components/App.tsx b/src/components/App.tsx index e03739c3..4c74d85d 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -27,17 +27,15 @@ import ModalDebug from './ModalDebug' import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata' 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 { createStyleStore, type IStyleStore } from '../libs/store/style-store-factory' 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 { SortEnd } from 'react-sortable-hoc'; import { MapOptions } from 'maplibre-gl'; +import { OnStyleChangedOpts, StyleSpecificationWithId } from '../libs/definitions' // Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed. window.Buffer = buffer.Buffer; @@ -83,12 +81,6 @@ function updateRootSpec(spec: any, fieldName: string, newValues: any) { } } -type OnStyleChangedOpts = { - save?: boolean - addRevision?: boolean - initialLoad?: boolean -} - type MappedErrors = { message: string parsed?: { @@ -104,7 +96,7 @@ type MappedErrors = { type AppState = { errors: MappedErrors[], infos: string[], - mapStyle: StyleSpecification & {id: string}, + mapStyle: StyleSpecificationWithId, dirtyMapStyle?: StyleSpecification, selectedLayerIndex: number, selectedLayerOriginalId?: string, @@ -140,25 +132,56 @@ type AppState = { export default class App extends React.Component { revisionStore: RevisionStore; - styleStore: StyleStore | ApiStyleStore; + styleStore: IStyleStore | null = null; layerWatcher: LayerWatcher; constructor(props: any) { super(props) - 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")) { - port = window.location.port + this.revisionStore = new RevisionStore(); + this.configureKeyboardShortcuts(); + + this.state = { + errors: [], + infos: [], + mapStyle: style.emptyStyle, + selectedLayerIndex: 0, + sources: {}, + vectorLayers: {}, + mapState: "map", + spec: latest, + mapView: { + zoom: 0, + center: { + lng: 0, + lat: 0, + }, + }, + isOpen: { + settings: false, + sources: false, + open: false, + shortcuts: false, + export: false, + debug: false, + }, + maplibreGlDebugOptions: { + showTileBoundaries: false, + showCollisionBoxes: false, + showOverdrawInspector: false, + }, + openlayersDebugOptions: { + debugToolbox: false, + }, + fileHandle: null, } - this.styleStore = new ApiStyleStore({ - onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, {save: false}), - port: port, - host: params.get("localhost") + + this.layerWatcher = new LayerWatcher({ + onVectorLayersChange: v => this.setState({ vectorLayers: v }) }) + } - + configureKeyboardShortcuts = () => { const shortcuts = [ { key: "?", @@ -228,74 +251,6 @@ export default class App extends React.Component { } } }) - - 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, {initialLoad: true})) - - 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); - } - - this.state = { - errors: [], - infos: [], - mapStyle: style.emptyStyle, - selectedLayerIndex: 0, - sources: {}, - vectorLayers: {}, - mapState: "map", - spec: latest, - mapView: { - zoom: 0, - center: { - lng: 0, - lat: 0, - }, - }, - isOpen: { - settings: false, - sources: false, - open: false, - shortcuts: false, - export: false, - // TODO: Disabled for now, this should be opened on the Nth visit to the editor - debug: false, - }, - maplibreGlDebugOptions: { - showTileBoundaries: false, - showCollisionBoxes: false, - showOverdrawInspector: false, - }, - openlayersDebugOptions: { - debugToolbox: false, - }, - fileHandle: null, - } - - this.layerWatcher = new LayerWatcher({ - onVectorLayersChange: v => this.setState({ vectorLayers: v }) - }) } handleKeyPress = (e: KeyboardEvent) => { @@ -321,7 +276,8 @@ export default class App extends React.Component { } } - componentDidMount() { + async componentDidMount() { + this.styleStore = await createStyleStore((mapStyle, opts) => this.onStyleChanged(mapStyle, opts)); window.addEventListener("keydown", this.handleKeyPress); } @@ -329,8 +285,8 @@ export default class App extends React.Component { window.removeEventListener("keydown", this.handleKeyPress); } - saveStyle(snapshotStyle: StyleSpecification & {id: string}) { - this.styleStore.save(snapshotStyle) + saveStyle(snapshotStyle: StyleSpecificationWithId) { + this.styleStore?.save(snapshotStyle) } updateFonts(urlTemplate: string) { @@ -371,7 +327,7 @@ export default class App extends React.Component { this.onStyleChanged(changedStyle) } - onStyleChanged = (newStyle: StyleSpecification & {id: string}, opts: OnStyleChangedOpts={}) => { + onStyleChanged = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}): void => { opts = { save: true, addRevision: true, @@ -507,7 +463,7 @@ export default class App extends React.Component { this.revisionStore.addRevision(newStyle); } if (opts.save) { - this.saveStyle(newStyle as StyleSpecification & {id: string}); + this.saveStyle(newStyle); } this.setState({ @@ -620,7 +576,7 @@ export default class App extends React.Component { }, this.setStateInUrl); } - setDefaultValues = (styleObj: StyleSpecification & {id: string}) => { + setDefaultValues = (styleObj: StyleSpecificationWithId) => { const metadata: {[key: string]: string} = styleObj.metadata || {} as any if(metadata['maputnik:renderer'] === undefined) { const changedStyle = { @@ -636,7 +592,7 @@ export default class App extends React.Component { } } - openStyle = (styleObj: StyleSpecification & {id: string}, fileHandle: FileSystemFileHandle | null) => { + openStyle = (styleObj: StyleSpecificationWithId, fileHandle: FileSystemFileHandle | null) => { this.setState({fileHandle: fileHandle}); styleObj = this.setDefaultValues(styleObj) this.onStyleChanged(styleObj) diff --git a/src/components/AppToolbar.tsx b/src/components/AppToolbar.tsx index ca12aa50..2844c4bf 100644 --- a/src/components/AppToolbar.tsx +++ b/src/components/AppToolbar.tsx @@ -16,6 +16,7 @@ import pkgJson from '../../package.json' import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline' import { withTranslation, WithTranslation } from 'react-i18next'; import { supportedLanguages } from '../i18n'; +import type { OnStyleChangedCallback } from '../libs/definitions'; // This is required because of , there isn't another way to detect support that I'm aware of. const browser = detect(); @@ -93,9 +94,9 @@ export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deut type AppToolbarInternalProps = { mapStyle: object inspectModeEnabled: boolean - onStyleChanged(...args: unknown[]): unknown + onStyleChanged: OnStyleChangedCallback // A new style has been uploaded - onStyleOpen(...args: unknown[]): unknown + onStyleOpen: OnStyleChangedCallback // A dict of source id's and the available source layers sources: object children?: React.ReactNode diff --git a/src/components/FieldType.tsx b/src/components/FieldType.tsx index f89be9f7..4f939c4f 100644 --- a/src/components/FieldType.tsx +++ b/src/components/FieldType.tsx @@ -14,32 +14,34 @@ type FieldTypeInternalProps = { disabled?: boolean } & WithTranslation; -const FieldTypeInternal: React.FC = (props) => { - const t = props.t; +const FieldTypeInternal: React.FC = ({ + t, + value, + wdKey, + onChange, + error, + disabled = false +}) => { const layerstypes: [string, string][] = Object.keys(v8.layer.type.values || {}).map(v => [v, startCase(v.replace(/-/g, ' '))]); return ( - {props.disabled && ( - + {disabled && ( + )} - {!props.disabled && ( + {!disabled && ( )} ); }; -FieldTypeInternal.defaultProps = { - disabled: false, -}; - const FieldType = withTranslation()(FieldTypeInternal); export default FieldType; diff --git a/src/components/FilterEditor.tsx b/src/components/FilterEditor.tsx index 0b6105a8..801ef21d 100644 --- a/src/components/FilterEditor.tsx +++ b/src/components/FilterEditor.tsx @@ -1,7 +1,7 @@ import React from 'react' import {mdiTableRowPlusAfter} from '@mdi/js'; import {isEqual} from 'lodash'; -import {ExpressionSpecification, LegacyFilterSpecification, StyleSpecification} from 'maplibre-gl' +import {ExpressionSpecification, LegacyFilterSpecification} from 'maplibre-gl' import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec' import {mdiFunctionVariant} from '@mdi/js'; @@ -14,6 +14,7 @@ import InputButton from './InputButton' import Doc from './Doc' import ExpressionProperty from './_ExpressionProperty'; import { WithTranslation, withTranslation } from 'react-i18next'; +import type { StyleSpecificationWithId } from '../libs/definitions'; function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification { @@ -39,7 +40,7 @@ function migrateFilter(filter: LegacyFilterSpecification | ExpressionSpecificati return (migrate(createStyleFromFilter(filter) as any).layers[0] as any).filter; } -function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecification & {id: string} { +function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecificationWithId { return { "id": "tmp", "version": 8, diff --git a/src/components/ModalExport.tsx b/src/components/ModalExport.tsx index 32046191..540b3d61 100644 --- a/src/components/ModalExport.tsx +++ b/src/components/ModalExport.tsx @@ -3,7 +3,6 @@ import Slugify from 'slugify' import {saveAs} from 'file-saver' import {version} from 'maplibre-gl/package.json' import {format} from '@maplibre/maplibre-gl-style-spec' -import type {StyleSpecification} from 'maplibre-gl' import {MdMap, MdSave} from 'react-icons/md' import {WithTranslation, withTranslation} from 'react-i18next'; @@ -12,6 +11,7 @@ import InputButton from './InputButton' import Modal from './Modal' import style from '../libs/style' import fieldSpecAdditional from '../libs/field-spec-additional' +import type {OnStyleChangedCallback, StyleSpecificationWithId} from '../libs/definitions' const MAPLIBRE_GL_VERSION = version; @@ -19,8 +19,8 @@ const showSaveFilePickerAvailable = typeof window.showSaveFilePicker === "functi type ModalExportInternalProps = { - mapStyle: StyleSpecification & { id: string } - onStyleChanged(...args: unknown[]): unknown + mapStyle: StyleSpecificationWithId + onStyleChanged: OnStyleChangedCallback isOpen: boolean onOpenToggle(...args: unknown[]): unknown onSetFileHandle(fileHandle: FileSystemFileHandle | null): unknown diff --git a/src/components/ModalSettings.tsx b/src/components/ModalSettings.tsx index 7f309bc4..504d1ed2 100644 --- a/src/components/ModalSettings.tsx +++ b/src/components/ModalSettings.tsx @@ -12,10 +12,11 @@ import FieldEnum from './FieldEnum' import FieldColor from './FieldColor' import Modal from './Modal' import fieldSpecAdditional from '../libs/field-spec-additional' +import type {OnStyleChangedCallback, StyleSpecificationWithId} from '../libs/definitions'; type ModalSettingsInternalProps = { - mapStyle: StyleSpecification - onStyleChanged(...args: unknown[]): unknown + mapStyle: StyleSpecificationWithId + onStyleChanged: OnStyleChangedCallback onChangeMetadataProperty(...args: unknown[]): unknown isOpen: boolean onOpenToggle(...args: unknown[]): unknown @@ -62,7 +63,7 @@ class ModalSettingsInternal extends React.Component changeTerrainProperty(property: keyof TerrainSpecification, value: any) { const terrain = { ...this.props.mapStyle.terrain, - } + } as TerrainSpecification; if (value === undefined) { delete terrain[property]; diff --git a/src/components/ModalSources.tsx b/src/components/ModalSources.tsx index 3db41bad..b0e542bc 100644 --- a/src/components/ModalSources.tsx +++ b/src/components/ModalSources.tsx @@ -1,7 +1,7 @@ import React from 'react' import {MdAddCircleOutline, MdDelete} from 'react-icons/md' import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' -import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, StyleSpecification, VectorSourceSpecification} from 'maplibre-gl' +import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, VectorSourceSpecification} from 'maplibre-gl' import { WithTranslation, withTranslation } from 'react-i18next'; import Modal from './Modal' @@ -13,6 +13,7 @@ import ModalSourcesTypeEditor, { EditorMode } from './ModalSourcesTypeEditor' import style from '../libs/style' import { deleteSource, addSource, changeSource } from '../libs/source' import publicSources from '../config/tilesets.json' +import { OnStyleChangedCallback, StyleSpecificationWithId } from '../libs/definitions'; type PublicSourceProps = { @@ -270,10 +271,10 @@ class AddSource extends React.Component { } type ModalSourcesInternalProps = { - mapStyle: StyleSpecification + mapStyle: StyleSpecificationWithId isOpen: boolean onOpenToggle(...args: unknown[]): unknown - onStyleChanged(...args: unknown[]): unknown + onStyleChanged: OnStyleChangedCallback } & WithTranslation; class ModalSourcesInternal extends React.Component { diff --git a/src/libs/debug.ts b/src/libs/debug.ts deleted file mode 100644 index db2e1f81..00000000 --- a/src/libs/debug.ts +++ /dev/null @@ -1,48 +0,0 @@ -interface DebugStore { - [namespace: string]: { - [key: string]: any - } -} - -const debugStore: DebugStore = {}; - -function enabled() { - const qs = new URL(window.location.href).searchParams; - const debugQs = qs.get("debug"); - if(debugQs) { - return !!debugQs.match(/^(|1|true)$/); - } - else { - return false; - } -} - -function genErr() { - return new Error("Debug not enabled, enable by appending '?debug' to your query string"); -} - -function set(namespace: keyof DebugStore, key: string, value: any) { - if(!enabled()) { - throw genErr(); - } - debugStore[namespace] = debugStore[namespace] || {}; - debugStore[namespace][key] = value; -} - -function get(namespace: keyof DebugStore, key: string) { - if(!enabled()) { - throw genErr(); - } - if(Object.prototype.hasOwnProperty.call(debugStore, namespace)) { - return debugStore[namespace][key]; - } -} - -const mod = { - enabled, - get, - set -}; - -(window as any).debug = mod; -export default mod; diff --git a/src/libs/definitions.d.ts b/src/libs/definitions.d.ts new file mode 100644 index 00000000..d2725031 --- /dev/null +++ b/src/libs/definitions.d.ts @@ -0,0 +1,16 @@ +import type { StyleSpecification } from "maplibre-gl"; + +export type StyleSpecificationWithId = StyleSpecification & {id: string}; + +export type OnStyleChangedOpts = { + save?: boolean; + addRevision?: boolean; + initialLoad?: boolean; +} + +export type OnStyleChangedCallback = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}) => void; + +export interface IStyleStore { + getLatestStyle(): Promise; + save(mapStyle: StyleSpecificationWithId): StyleSpecificationWithId; +} diff --git a/src/libs/revisions.ts b/src/libs/revisions.ts index fd1b4180..c03b68ad 100644 --- a/src/libs/revisions.ts +++ b/src/libs/revisions.ts @@ -1,7 +1,7 @@ -import type {StyleSpecification} from "maplibre-gl"; +import { StyleSpecificationWithId } from "./definitions"; export class RevisionStore { - revisions: (StyleSpecification & {id: string})[]; + revisions: StyleSpecificationWithId[]; currentIdx: number; @@ -18,7 +18,7 @@ export class RevisionStore { return this.revisions[this.currentIdx] } - addRevision(revision: StyleSpecification & {id: string}) { + addRevision(revision: StyleSpecificationWithId) { // clear any "redo" revisions once a change is made // and ensure current index is at end of list this.revisions = this.revisions.slice(0, this.currentIdx + 1); diff --git a/src/libs/source.ts b/src/libs/source.ts index 838465e5..a9737c70 100644 --- a/src/libs/source.ts +++ b/src/libs/source.ts @@ -1,6 +1,7 @@ -import type {StyleSpecification, SourceSpecification} from "maplibre-gl"; +import type {SourceSpecification} from "maplibre-gl"; +import type {StyleSpecificationWithId} from "./definitions"; -export function deleteSource(mapStyle: StyleSpecification, sourceId: string) { +export function deleteSource(mapStyle: StyleSpecificationWithId, sourceId: string) { const remainingSources = { ...mapStyle.sources} delete remainingSources[sourceId] return { @@ -10,11 +11,11 @@ export function deleteSource(mapStyle: StyleSpecification, sourceId: string) { } -export function addSource(mapStyle: StyleSpecification, sourceId: string, source: SourceSpecification) { +export function addSource(mapStyle: StyleSpecificationWithId, sourceId: string, source: SourceSpecification) { return changeSource(mapStyle, sourceId, source) } -export function changeSource(mapStyle: StyleSpecification, sourceId: string, source: SourceSpecification) { +export function changeSource(mapStyle: StyleSpecificationWithId, sourceId: string, source: SourceSpecification) { const changedSources = { ...mapStyle.sources, [sourceId]: source diff --git a/src/libs/apistore.ts b/src/libs/store/apistore.ts similarity index 60% rename from src/libs/apistore.ts rename to src/libs/store/apistore.ts index 62d5849a..5357a52b 100644 --- a/src/libs/apistore.ts +++ b/src/libs/store/apistore.ts @@ -1,46 +1,38 @@ -import style from './style.js' +import style from '../style' import {format} from '@maplibre/maplibre-gl-style-spec' -import type {StyleSpecification} from 'maplibre-gl' import ReconnectingWebSocket from 'reconnecting-websocket' +import type {IStyleStore, OnStyleChangedCallback, StyleSpecificationWithId} from '../definitions' export type ApiStyleStoreOptions = { - port: string | null - host: string | null - onLocalStyleChange?: (style: any) => void + onLocalStyleChange?: OnStyleChangedCallback } -export class ApiStyleStore { +export class ApiStyleStore implements IStyleStore { localUrl: string; websocketUrl: string; latestStyleId: string | undefined = undefined; - onLocalStyleChange: (style: any) => void; + onLocalStyleChange: OnStyleChangedCallback; constructor(opts: ApiStyleStoreOptions) { this.onLocalStyleChange = opts.onLocalStyleChange || (() => {}) - const port = opts.port || '8000' - const host = opts.host || 'localhost' + const port = window.location.port + const host = 'localhost' this.localUrl = `http://${host}:${port}` this.websocketUrl = `ws://${host}:${port}/ws` this.init = this.init.bind(this) } - init(cb: (...args: any[]) => void) { - fetch(this.localUrl + '/styles', { - mode: 'cors', - }) - .then((response) => { - return response.json(); - }) - .then((body) => { - const styleIds = body; - this.latestStyleId = styleIds[0] - this.notifyLocalChanges() - cb(null) - }) - .catch(() => { - cb(new Error('Can not connect to style API')) - }) + async init(): Promise { + try { + const response = await fetch(this.localUrl + '/styles', {mode: 'cors'}); + const body = await response.json(); + const styleIds = body; + this.latestStyleId = styleIds[0] + this.notifyLocalChanges(); + } catch { + throw new Error('Can not connect to style API'); + } } notifyLocalChanges() { @@ -59,24 +51,20 @@ export class ApiStyleStore { } } - latestStyle(cb: (...args: any[]) => void) { + async getLatestStyle(): Promise { if(this.latestStyleId) { - fetch(this.localUrl + '/styles/' + this.latestStyleId, { + const response = await fetch(this.localUrl + '/styles/' + this.latestStyleId, { mode: 'cors', - }) - .then(function(response) { - return response.json(); - }) - .then(function(body) { - cb(style.ensureStyleValidity(body)) - }) + }); + const body = await response.json(); + return style.ensureStyleValidity(body); } else { throw new Error('No latest style available. You need to init the api backend first.') } } // Save current style replacing previous version - save(mapStyle: StyleSpecification & { id: string }) { + save(mapStyle: StyleSpecificationWithId) { const styleJSON = format( style.stripAccessTokens( style.replaceAccessTokens(mapStyle) diff --git a/src/libs/store/style-store-factory.ts b/src/libs/store/style-store-factory.ts new file mode 100644 index 00000000..f20657d7 --- /dev/null +++ b/src/libs/store/style-store-factory.ts @@ -0,0 +1,29 @@ +/// +import { IStyleStore, OnStyleChangedCallback } from "../definitions"; +import { getStyleUrlFromAddressbarAndRemoveItIfNeeded, loadStyleUrl } from "../urlopen"; +import { ApiStyleStore } from "./apistore"; +import { StyleStore } from "./stylestore"; + +export async function createStyleStore(onStyleChanged: OnStyleChangedCallback): Promise { + const styleUrl = getStyleUrlFromAddressbarAndRemoveItIfNeeded(); + const useStyleUrl = styleUrl && window.confirm("Load style from URL: " + styleUrl + " and discard current changes?"); + let styleStore: IStyleStore; + if (import.meta.env.MODE === 'desktop' && !useStyleUrl) { + const apiStyleStore = new ApiStyleStore({ + onLocalStyleChange: mapStyle => onStyleChanged(mapStyle, {save: false}), + }); + try { + await apiStyleStore.init(); + styleStore = apiStyleStore; + } catch { + styleStore = new StyleStore(); + } + } else { + styleStore = new StyleStore(); + } + const styleToLoad = useStyleUrl ? await loadStyleUrl(styleUrl) : await styleStore.getLatestStyle(); + onStyleChanged(styleToLoad, {initialLoad: true, save: false}); + return styleStore; +} + +export type { IStyleStore }; diff --git a/src/libs/stylestore.ts b/src/libs/store/stylestore.ts similarity index 79% rename from src/libs/stylestore.ts rename to src/libs/store/stylestore.ts index a97a74cf..49f5e469 100644 --- a/src/libs/stylestore.ts +++ b/src/libs/store/stylestore.ts @@ -1,7 +1,7 @@ -import style from './style' -import {loadStyleUrl} from './urlopen' -import publicSources from '../config/styles.json' -import type {StyleSpecification} from 'maplibre-gl' +import style from '../style' +import {loadStyleUrl} from '../urlopen' +import publicSources from '../../config/styles.json' +import type {IStyleStore, StyleSpecificationWithId} from '../definitions' const storagePrefix = "maputnik" const stylePrefix = 'style' @@ -13,8 +13,8 @@ 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: (...args: any[]) => void) { - loadStyleUrl(defaultStyleUrl, cb) +export function loadDefaultStyle(): Promise { + return loadStyleUrl(defaultStyleUrl); } // Return style ids and dates of all styles stored in local storage @@ -51,7 +51,7 @@ function styleKey(styleId: string) { } // Manages many possible styles that are stored in the local storage -export class StyleStore { +export class StyleStore implements IStyleStore { /** * List of style ids */ @@ -63,10 +63,6 @@ export class StyleStore { this.mapStyles = loadStoredStyles(); } - init(cb: (...args: any[]) => void) { - cb(null) - } - // Delete entire style history purge() { for (let i = 0; i < window.localStorage.length; i++) { @@ -78,17 +74,21 @@ export class StyleStore { } // Find the last edited style - latestStyle(cb: (...args: any[]) => void) { - if(this.mapStyles.length === 0) return loadDefaultStyle(cb) + async getLatestStyle(): Promise { + if(this.mapStyles.length === 0) { + return loadDefaultStyle(); + } const styleId = window.localStorage.getItem(storageKeys.latest) as string; const styleItem = window.localStorage.getItem(styleKey(styleId)) - if(styleItem) return cb(JSON.parse(styleItem)) - loadDefaultStyle(cb) + if (styleItem) { + return JSON.parse(styleItem) as StyleSpecificationWithId; + } + return loadDefaultStyle(); } // Save current style replacing previous version - save(mapStyle: StyleSpecification & { id: string }) { + save(mapStyle: StyleSpecificationWithId) { mapStyle = style.ensureStyleValidity(mapStyle) const key = styleKey(mapStyle.id) diff --git a/src/libs/style.ts b/src/libs/style.ts index 9fd6aff6..31f8cf52 100644 --- a/src/libs/style.ts +++ b/src/libs/style.ts @@ -1,6 +1,7 @@ import {derefLayers} from '@maplibre/maplibre-gl-style-spec' import type {StyleSpecification, LayerSpecification} from 'maplibre-gl' import tokens from '../config/tokens.json' +import type {StyleSpecificationWithId} from './definitions' // Empty style is always used if no style could be restored or fetched const emptyStyle = ensureStyleValidity({ @@ -13,15 +14,14 @@ function generateId() { return Math.random().toString(36).substring(2, 9) } -function ensureHasId(style: StyleSpecification & { id?: string }): StyleSpecification & { id: string } { +function ensureHasId(style: StyleSpecification & { id?: string }): StyleSpecificationWithId { if(!('id' in style) || !style.id) { style.id = generateId(); - return style as StyleSpecification & { id: string }; } - return style as StyleSpecification & { id: string }; + return style as StyleSpecificationWithId; } -function ensureHasNoInteractive(style: StyleSpecification & {id: string}) { +function ensureHasNoInteractive(style: StyleSpecificationWithId) { const changedLayers = style.layers.map(layer => { const changedLayer: LayerSpecification & { interactive?: any } = { ...layer } delete changedLayer.interactive @@ -34,14 +34,14 @@ function ensureHasNoInteractive(style: StyleSpecification & {id: string}) { } } -function ensureHasNoRefs(style: StyleSpecification & {id: string}) { +function ensureHasNoRefs(style: StyleSpecificationWithId) { return { ...style, layers: derefLayers(style.layers) } } -function ensureStyleValidity(style: StyleSpecification): StyleSpecification & { id: string } { +function ensureStyleValidity(style: StyleSpecification): StyleSpecificationWithId { return ensureHasNoInteractive(ensureHasNoRefs(ensureHasId(style))) } diff --git a/src/libs/urlopen.ts b/src/libs/urlopen.ts index dba2246e..b7426227 100644 --- a/src/libs/urlopen.ts +++ b/src/libs/urlopen.ts @@ -1,30 +1,27 @@ import style from './style' +import { StyleSpecificationWithId } from './definitions'; -export function initialStyleUrl() { +export function getStyleUrlFromAddressbarAndRemoveItIfNeeded(): string | null { const initialUrl = new URL(window.location.href); - return initialUrl.searchParams.get('style'); + const styleUrl = initialUrl.searchParams.get('style'); + if (styleUrl) { + initialUrl.searchParams.delete('style'); + window.history.replaceState({}, document.title, initialUrl.toString()) + } + return styleUrl; } -export function loadStyleUrl(styleUrl: string, cb: (...args: any[]) => void) { +export async function loadStyleUrl(styleUrl: string): Promise { console.log('Loading style', styleUrl) - fetch(styleUrl, { - mode: 'cors', - credentials: "same-origin" - }) - .then(function(response) { - return response.json(); - }) - .then(function(body) { - cb(style.ensureStyleValidity(body)) - }) - .catch(function() { - console.warn('Could not fetch default style', styleUrl) - cb(style.emptyStyle) - }) -} - -export function removeStyleQuerystring() { - const initialUrl = new URL(window.location.href); - initialUrl.searchParams.delete('style'); - window.history.replaceState({}, document.title, initialUrl.toString()) + try { + const response = await fetch(styleUrl, { + mode: 'cors', + credentials: "same-origin" + }); + const body = await response.json(); + return style.ensureStyleValidity(body); + } catch { + console.warn('Could not fetch default style: ' + styleUrl) + return style.emptyStyle + } } diff --git a/vite.config.ts b/vite.config.ts index 3d36e180..19b9b7c1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,7 +3,7 @@ import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import istanbul from "vite-plugin-istanbul"; -export default defineConfig({ +export default defineConfig(({ mode }) => ({ server: { port: 8888, }, @@ -27,7 +27,8 @@ export default defineConfig({ forceBuildInstrument: true, //Instrument the source code for cypress runs }), ], + base: mode === "desktop" ? "/" : "/maputnik/", define: { - global: "window", + global: "window" }, -}); +}));