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" }, -}); +}));