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:
<img width="1263" height="439" alt="image"
src="https://github.com/user-attachments/assets/1988c4f7-39de-4fd2-b6da-b4736abc0441"
/>

After:
<img width="1263" height="203" alt="image"
src="https://github.com/user-attachments/assets/28079e6d-9de7-40a1-9869-01a0876ca79f"
/>

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bart Louwers <bart.louwers@gmail.com>
This commit is contained in:
Harel M
2025-09-08 15:22:29 +03:00
committed by GitHub
parent 6f4c34b29a
commit abe6230932
21 changed files with 218 additions and 272 deletions

View File

@@ -17,6 +17,7 @@
- Refactor Field components to use arrow function syntax - Refactor Field components to use arrow function syntax
- Replace react-autocomplete with Downshift in the autocomplete component - Replace react-autocomplete with Downshift in the autocomplete component
- Add LocationIQ as supported map provider with access token field and gallery style - 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..._ - _...Add new stuff here..._
### 🐞 Bug fixes ### 🐞 Bug fixes

View File

@@ -16,7 +16,6 @@ describe("history", () => {
it("undo/redo", () => { it("undo/redo", () => {
when.setStyle("geojson"); when.setStyle("geojson");
when.modal.open(); when.modal.open();
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
when.modal.fillLayers({ when.modal.fillLayers({
id: "step 1", id: "step 1",

View File

@@ -110,25 +110,25 @@ export class MaputnikDriver {
styleProperties: "geojson" | "raster" | "both" | "layer" | "", styleProperties: "geojson" | "raster" | "both" | "layer" | "",
zoom?: number zoom?: number
) => { ) => {
let url = "?debug"; const url = new URL(baseUrl);
switch (styleProperties) { switch (styleProperties) {
case "geojson": case "geojson":
url += `&style=${baseUrl}geojson-style.json`; url.searchParams.set("style", baseUrl + "geojson-style.json");
break; break;
case "raster": case "raster":
url += `&style=${baseUrl}raster-style.json`; url.searchParams.set("style", baseUrl + "raster-style.json");
break; break;
case "both": case "both":
url += `&style=${baseUrl}geojson-raster-style.json`; url.searchParams.set("style", baseUrl + "geojson-raster-style.json");
break; break;
case "layer": case "layer":
url += `&style=${baseUrl}/example-layer-style.json`; url.searchParams.set("style", baseUrl + "example-layer-style.json");
break; break;
} }
if (zoom) { 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) { if (styleProperties) {
this.helper.when.acceptConfirm(); this.helper.when.acceptConfirm();
} }

View File

@@ -6,8 +6,8 @@
"main": "''", "main": "''",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
"build": "tsc && vite build --base=/maputnik/", "build": "tsc && vite build --mode=production",
"build-desktop": "tsc && vite build --base=/ && cd desktop && make", "build-desktop": "tsc && vite build --mode=desktop && cd desktop && make",
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'", "i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
"lint": "eslint", "lint": "eslint",
"test": "cypress run", "test": "cypress run",

View File

@@ -27,17 +27,15 @@ import ModalDebug from './ModalDebug'
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata' import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
import style from '../libs/style' import style from '../libs/style'
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
import { undoMessages, redoMessages } from '../libs/diffmessage' import { undoMessages, redoMessages } from '../libs/diffmessage'
import { StyleStore } from '../libs/stylestore' import { createStyleStore, type IStyleStore } from '../libs/store/style-store-factory'
import { ApiStyleStore } from '../libs/apistore'
import { RevisionStore } from '../libs/revisions' import { RevisionStore } from '../libs/revisions'
import LayerWatcher from '../libs/layerwatcher' import LayerWatcher from '../libs/layerwatcher'
import tokens from '../config/tokens.json' import tokens from '../config/tokens.json'
import isEqual from 'lodash.isequal' import isEqual from 'lodash.isequal'
import Debug from '../libs/debug'
import { SortEnd } from 'react-sortable-hoc'; import { SortEnd } from 'react-sortable-hoc';
import { MapOptions } from 'maplibre-gl'; 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. // Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
window.Buffer = buffer.Buffer; 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 = { type MappedErrors = {
message: string message: string
parsed?: { parsed?: {
@@ -104,7 +96,7 @@ type MappedErrors = {
type AppState = { type AppState = {
errors: MappedErrors[], errors: MappedErrors[],
infos: string[], infos: string[],
mapStyle: StyleSpecification & {id: string}, mapStyle: StyleSpecificationWithId,
dirtyMapStyle?: StyleSpecification, dirtyMapStyle?: StyleSpecification,
selectedLayerIndex: number, selectedLayerIndex: number,
selectedLayerOriginalId?: string, selectedLayerOriginalId?: string,
@@ -140,25 +132,56 @@ type AppState = {
export default class App extends React.Component<any, AppState> { export default class App extends React.Component<any, AppState> {
revisionStore: RevisionStore; revisionStore: RevisionStore;
styleStore: StyleStore | ApiStyleStore; styleStore: IStyleStore | null = null;
layerWatcher: LayerWatcher; layerWatcher: LayerWatcher;
constructor(props: any) { constructor(props: any) {
super(props) super(props)
this.revisionStore = new RevisionStore() this.revisionStore = new RevisionStore();
const params = new URLSearchParams(window.location.search.substring(1)) this.configureKeyboardShortcuts();
let port = params.get("localport")
if (port == null && (window.location.port !== "80" && window.location.port !== "443")) { this.state = {
port = window.location.port 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}), this.layerWatcher = new LayerWatcher({
port: port, onVectorLayersChange: v => this.setState({ vectorLayers: v })
host: params.get("localhost")
}) })
}
configureKeyboardShortcuts = () => {
const shortcuts = [ const shortcuts = [
{ {
key: "?", key: "?",
@@ -228,74 +251,6 @@ export default class App extends React.Component<any, AppState> {
} }
} }
}) })
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) => { handleKeyPress = (e: KeyboardEvent) => {
@@ -321,7 +276,8 @@ export default class App extends React.Component<any, AppState> {
} }
} }
componentDidMount() { async componentDidMount() {
this.styleStore = await createStyleStore((mapStyle, opts) => this.onStyleChanged(mapStyle, opts));
window.addEventListener("keydown", this.handleKeyPress); window.addEventListener("keydown", this.handleKeyPress);
} }
@@ -329,8 +285,8 @@ export default class App extends React.Component<any, AppState> {
window.removeEventListener("keydown", this.handleKeyPress); window.removeEventListener("keydown", this.handleKeyPress);
} }
saveStyle(snapshotStyle: StyleSpecification & {id: string}) { saveStyle(snapshotStyle: StyleSpecificationWithId) {
this.styleStore.save(snapshotStyle) this.styleStore?.save(snapshotStyle)
} }
updateFonts(urlTemplate: string) { updateFonts(urlTemplate: string) {
@@ -371,7 +327,7 @@ export default class App extends React.Component<any, AppState> {
this.onStyleChanged(changedStyle) this.onStyleChanged(changedStyle)
} }
onStyleChanged = (newStyle: StyleSpecification & {id: string}, opts: OnStyleChangedOpts={}) => { onStyleChanged = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}): void => {
opts = { opts = {
save: true, save: true,
addRevision: true, addRevision: true,
@@ -507,7 +463,7 @@ export default class App extends React.Component<any, AppState> {
this.revisionStore.addRevision(newStyle); this.revisionStore.addRevision(newStyle);
} }
if (opts.save) { if (opts.save) {
this.saveStyle(newStyle as StyleSpecification & {id: string}); this.saveStyle(newStyle);
} }
this.setState({ this.setState({
@@ -620,7 +576,7 @@ export default class App extends React.Component<any, AppState> {
}, this.setStateInUrl); }, this.setStateInUrl);
} }
setDefaultValues = (styleObj: StyleSpecification & {id: string}) => { setDefaultValues = (styleObj: StyleSpecificationWithId) => {
const metadata: {[key: string]: string} = styleObj.metadata || {} as any const metadata: {[key: string]: string} = styleObj.metadata || {} as any
if(metadata['maputnik:renderer'] === undefined) { if(metadata['maputnik:renderer'] === undefined) {
const changedStyle = { const changedStyle = {
@@ -636,7 +592,7 @@ export default class App extends React.Component<any, AppState> {
} }
} }
openStyle = (styleObj: StyleSpecification & {id: string}, fileHandle: FileSystemFileHandle | null) => { openStyle = (styleObj: StyleSpecificationWithId, fileHandle: FileSystemFileHandle | null) => {
this.setState({fileHandle: fileHandle}); this.setState({fileHandle: fileHandle});
styleObj = this.setDefaultValues(styleObj) styleObj = this.setDefaultValues(styleObj)
this.onStyleChanged(styleObj) this.onStyleChanged(styleObj)

View File

@@ -16,6 +16,7 @@ import pkgJson from '../../package.json'
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline' import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
import { withTranslation, WithTranslation } from 'react-i18next'; import { withTranslation, WithTranslation } from 'react-i18next';
import { supportedLanguages } from '../i18n'; import { supportedLanguages } from '../i18n';
import type { OnStyleChangedCallback } from '../libs/definitions';
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of. // This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
const browser = detect(); const browser = detect();
@@ -93,9 +94,9 @@ export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deut
type AppToolbarInternalProps = { type AppToolbarInternalProps = {
mapStyle: object mapStyle: object
inspectModeEnabled: boolean inspectModeEnabled: boolean
onStyleChanged(...args: unknown[]): unknown onStyleChanged: OnStyleChangedCallback
// A new style has been uploaded // A new style has been uploaded
onStyleOpen(...args: unknown[]): unknown onStyleOpen: OnStyleChangedCallback
// A dict of source id's and the available source layers // A dict of source id's and the available source layers
sources: object sources: object
children?: React.ReactNode children?: React.ReactNode

View File

@@ -14,32 +14,34 @@ type FieldTypeInternalProps = {
disabled?: boolean disabled?: boolean
} & WithTranslation; } & WithTranslation;
const FieldTypeInternal: React.FC<FieldTypeInternalProps> = (props) => { const FieldTypeInternal: React.FC<FieldTypeInternalProps> = ({
const t = props.t; t,
value,
wdKey,
onChange,
error,
disabled = false
}) => {
const layerstypes: [string, string][] = Object.keys(v8.layer.type.values || {}).map(v => [v, startCase(v.replace(/-/g, ' '))]); const layerstypes: [string, string][] = Object.keys(v8.layer.type.values || {}).map(v => [v, startCase(v.replace(/-/g, ' '))]);
return ( return (
<Block label={t('Type')} fieldSpec={v8.layer.type} <Block label={t('Type')} fieldSpec={v8.layer.type}
data-wd-key={props.wdKey} data-wd-key={wdKey}
error={props.error} error={error}
> >
{props.disabled && ( {disabled && (
<InputString value={props.value} disabled={true} /> <InputString value={value} disabled={true} />
)} )}
{!props.disabled && ( {!disabled && (
<InputSelect <InputSelect
options={layerstypes} options={layerstypes}
onChange={props.onChange} onChange={onChange}
value={props.value} value={value}
data-wd-key={props.wdKey + '.select'} data-wd-key={wdKey + '.select'}
/> />
)} )}
</Block> </Block>
); );
}; };
FieldTypeInternal.defaultProps = {
disabled: false,
};
const FieldType = withTranslation()(FieldTypeInternal); const FieldType = withTranslation()(FieldTypeInternal);
export default FieldType; export default FieldType;

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import {mdiTableRowPlusAfter} from '@mdi/js'; import {mdiTableRowPlusAfter} from '@mdi/js';
import {isEqual} from 'lodash'; 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 {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
import {mdiFunctionVariant} from '@mdi/js'; import {mdiFunctionVariant} from '@mdi/js';
@@ -14,6 +14,7 @@ import InputButton from './InputButton'
import Doc from './Doc' import Doc from './Doc'
import ExpressionProperty from './_ExpressionProperty'; import ExpressionProperty from './_ExpressionProperty';
import { WithTranslation, withTranslation } from 'react-i18next'; import { WithTranslation, withTranslation } from 'react-i18next';
import type { StyleSpecificationWithId } from '../libs/definitions';
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification { 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; 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 { return {
"id": "tmp", "id": "tmp",
"version": 8, "version": 8,

View File

@@ -3,7 +3,6 @@ import Slugify from 'slugify'
import {saveAs} from 'file-saver' import {saveAs} from 'file-saver'
import {version} from 'maplibre-gl/package.json' import {version} from 'maplibre-gl/package.json'
import {format} from '@maplibre/maplibre-gl-style-spec' import {format} from '@maplibre/maplibre-gl-style-spec'
import type {StyleSpecification} from 'maplibre-gl'
import {MdMap, MdSave} from 'react-icons/md' import {MdMap, MdSave} from 'react-icons/md'
import {WithTranslation, withTranslation} from 'react-i18next'; import {WithTranslation, withTranslation} from 'react-i18next';
@@ -12,6 +11,7 @@ import InputButton from './InputButton'
import Modal from './Modal' import Modal from './Modal'
import style from '../libs/style' import style from '../libs/style'
import fieldSpecAdditional from '../libs/field-spec-additional' import fieldSpecAdditional from '../libs/field-spec-additional'
import type {OnStyleChangedCallback, StyleSpecificationWithId} from '../libs/definitions'
const MAPLIBRE_GL_VERSION = version; const MAPLIBRE_GL_VERSION = version;
@@ -19,8 +19,8 @@ const showSaveFilePickerAvailable = typeof window.showSaveFilePicker === "functi
type ModalExportInternalProps = { type ModalExportInternalProps = {
mapStyle: StyleSpecification & { id: string } mapStyle: StyleSpecificationWithId
onStyleChanged(...args: unknown[]): unknown onStyleChanged: OnStyleChangedCallback
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(...args: unknown[]): unknown
onSetFileHandle(fileHandle: FileSystemFileHandle | null): unknown onSetFileHandle(fileHandle: FileSystemFileHandle | null): unknown

View File

@@ -12,10 +12,11 @@ import FieldEnum from './FieldEnum'
import FieldColor from './FieldColor' import FieldColor from './FieldColor'
import Modal from './Modal' import Modal from './Modal'
import fieldSpecAdditional from '../libs/field-spec-additional' import fieldSpecAdditional from '../libs/field-spec-additional'
import type {OnStyleChangedCallback, StyleSpecificationWithId} from '../libs/definitions';
type ModalSettingsInternalProps = { type ModalSettingsInternalProps = {
mapStyle: StyleSpecification mapStyle: StyleSpecificationWithId
onStyleChanged(...args: unknown[]): unknown onStyleChanged: OnStyleChangedCallback
onChangeMetadataProperty(...args: unknown[]): unknown onChangeMetadataProperty(...args: unknown[]): unknown
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(...args: unknown[]): unknown
@@ -62,7 +63,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
changeTerrainProperty(property: keyof TerrainSpecification, value: any) { changeTerrainProperty(property: keyof TerrainSpecification, value: any) {
const terrain = { const terrain = {
...this.props.mapStyle.terrain, ...this.props.mapStyle.terrain,
} } as TerrainSpecification;
if (value === undefined) { if (value === undefined) {
delete terrain[property]; delete terrain[property];

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import {MdAddCircleOutline, MdDelete} from 'react-icons/md' import {MdAddCircleOutline, MdDelete} from 'react-icons/md'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json' 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 { WithTranslation, withTranslation } from 'react-i18next';
import Modal from './Modal' import Modal from './Modal'
@@ -13,6 +13,7 @@ import ModalSourcesTypeEditor, { EditorMode } from './ModalSourcesTypeEditor'
import style from '../libs/style' import style from '../libs/style'
import { deleteSource, addSource, changeSource } from '../libs/source' import { deleteSource, addSource, changeSource } from '../libs/source'
import publicSources from '../config/tilesets.json' import publicSources from '../config/tilesets.json'
import { OnStyleChangedCallback, StyleSpecificationWithId } from '../libs/definitions';
type PublicSourceProps = { type PublicSourceProps = {
@@ -270,10 +271,10 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
} }
type ModalSourcesInternalProps = { type ModalSourcesInternalProps = {
mapStyle: StyleSpecification mapStyle: StyleSpecificationWithId
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(...args: unknown[]): unknown
onStyleChanged(...args: unknown[]): unknown onStyleChanged: OnStyleChangedCallback
} & WithTranslation; } & WithTranslation;
class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> { class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {

View File

@@ -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;

16
src/libs/definitions.d.ts vendored Normal file
View File

@@ -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<StyleSpecificationWithId>;
save(mapStyle: StyleSpecificationWithId): StyleSpecificationWithId;
}

View File

@@ -1,7 +1,7 @@
import type {StyleSpecification} from "maplibre-gl"; import { StyleSpecificationWithId } from "./definitions";
export class RevisionStore { export class RevisionStore {
revisions: (StyleSpecification & {id: string})[]; revisions: StyleSpecificationWithId[];
currentIdx: number; currentIdx: number;
@@ -18,7 +18,7 @@ export class RevisionStore {
return this.revisions[this.currentIdx] return this.revisions[this.currentIdx]
} }
addRevision(revision: StyleSpecification & {id: string}) { addRevision(revision: StyleSpecificationWithId) {
// clear any "redo" revisions once a change is made // clear any "redo" revisions once a change is made
// and ensure current index is at end of list // and ensure current index is at end of list
this.revisions = this.revisions.slice(0, this.currentIdx + 1); this.revisions = this.revisions.slice(0, this.currentIdx + 1);

View File

@@ -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} const remainingSources = { ...mapStyle.sources}
delete remainingSources[sourceId] delete remainingSources[sourceId]
return { 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) 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 = { const changedSources = {
...mapStyle.sources, ...mapStyle.sources,
[sourceId]: source [sourceId]: source

View File

@@ -1,46 +1,38 @@
import style from './style.js' import style from '../style'
import {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' import ReconnectingWebSocket from 'reconnecting-websocket'
import type {IStyleStore, OnStyleChangedCallback, StyleSpecificationWithId} from '../definitions'
export type ApiStyleStoreOptions = { export type ApiStyleStoreOptions = {
port: string | null onLocalStyleChange?: OnStyleChangedCallback
host: string | null
onLocalStyleChange?: (style: any) => void
} }
export class ApiStyleStore { export class ApiStyleStore implements IStyleStore {
localUrl: string; localUrl: string;
websocketUrl: string; websocketUrl: string;
latestStyleId: string | undefined = undefined; latestStyleId: string | undefined = undefined;
onLocalStyleChange: (style: any) => void; onLocalStyleChange: OnStyleChangedCallback;
constructor(opts: ApiStyleStoreOptions) { constructor(opts: ApiStyleStoreOptions) {
this.onLocalStyleChange = opts.onLocalStyleChange || (() => {}) this.onLocalStyleChange = opts.onLocalStyleChange || (() => {})
const port = opts.port || '8000' const port = window.location.port
const host = opts.host || 'localhost' const host = 'localhost'
this.localUrl = `http://${host}:${port}` this.localUrl = `http://${host}:${port}`
this.websocketUrl = `ws://${host}:${port}/ws` this.websocketUrl = `ws://${host}:${port}/ws`
this.init = this.init.bind(this) this.init = this.init.bind(this)
} }
init(cb: (...args: any[]) => void) { async init(): Promise<void> {
fetch(this.localUrl + '/styles', { try {
mode: 'cors', const response = await fetch(this.localUrl + '/styles', {mode: 'cors'});
}) const body = await response.json();
.then((response) => { const styleIds = body;
return response.json(); this.latestStyleId = styleIds[0]
}) this.notifyLocalChanges();
.then((body) => { } catch {
const styleIds = body; throw new Error('Can not connect to style API');
this.latestStyleId = styleIds[0] }
this.notifyLocalChanges()
cb(null)
})
.catch(() => {
cb(new Error('Can not connect to style API'))
})
} }
notifyLocalChanges() { notifyLocalChanges() {
@@ -59,24 +51,20 @@ export class ApiStyleStore {
} }
} }
latestStyle(cb: (...args: any[]) => void) { async getLatestStyle(): Promise<StyleSpecificationWithId> {
if(this.latestStyleId) { if(this.latestStyleId) {
fetch(this.localUrl + '/styles/' + this.latestStyleId, { const response = await fetch(this.localUrl + '/styles/' + this.latestStyleId, {
mode: 'cors', mode: 'cors',
}) });
.then(function(response) { const body = await response.json();
return response.json(); return style.ensureStyleValidity(body);
})
.then(function(body) {
cb(style.ensureStyleValidity(body))
})
} else { } else {
throw new Error('No latest style available. You need to init the api backend first.') throw new Error('No latest style available. You need to init the api backend first.')
} }
} }
// Save current style replacing previous version // Save current style replacing previous version
save(mapStyle: StyleSpecification & { id: string }) { save(mapStyle: StyleSpecificationWithId) {
const styleJSON = format( const styleJSON = format(
style.stripAccessTokens( style.stripAccessTokens(
style.replaceAccessTokens(mapStyle) style.replaceAccessTokens(mapStyle)

View File

@@ -0,0 +1,29 @@
/// <reference types="vite/client" />
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<IStyleStore> {
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 };

View File

@@ -1,7 +1,7 @@
import style from './style' import style from '../style'
import {loadStyleUrl} from './urlopen' import {loadStyleUrl} from '../urlopen'
import publicSources from '../config/styles.json' import publicSources from '../../config/styles.json'
import type {StyleSpecification} from 'maplibre-gl' import type {IStyleStore, StyleSpecificationWithId} from '../definitions'
const storagePrefix = "maputnik" const storagePrefix = "maputnik"
const stylePrefix = 'style' const stylePrefix = 'style'
@@ -13,8 +13,8 @@ const storageKeys = {
const defaultStyleUrl = publicSources[0].url const defaultStyleUrl = publicSources[0].url
// Fetch a default style via URL and return it or a fallback style via callback // Fetch a default style via URL and return it or a fallback style via callback
export function loadDefaultStyle(cb: (...args: any[]) => void) { export function loadDefaultStyle(): Promise<StyleSpecificationWithId> {
loadStyleUrl(defaultStyleUrl, cb) return loadStyleUrl(defaultStyleUrl);
} }
// Return style ids and dates of all styles stored in local storage // 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 // Manages many possible styles that are stored in the local storage
export class StyleStore { export class StyleStore implements IStyleStore {
/** /**
* List of style ids * List of style ids
*/ */
@@ -63,10 +63,6 @@ export class StyleStore {
this.mapStyles = loadStoredStyles(); this.mapStyles = loadStoredStyles();
} }
init(cb: (...args: any[]) => void) {
cb(null)
}
// Delete entire style history // Delete entire style history
purge() { purge() {
for (let i = 0; i < window.localStorage.length; i++) { for (let i = 0; i < window.localStorage.length; i++) {
@@ -78,17 +74,21 @@ export class StyleStore {
} }
// Find the last edited style // Find the last edited style
latestStyle(cb: (...args: any[]) => void) { async getLatestStyle(): Promise<StyleSpecificationWithId> {
if(this.mapStyles.length === 0) return loadDefaultStyle(cb) if(this.mapStyles.length === 0) {
return loadDefaultStyle();
}
const styleId = window.localStorage.getItem(storageKeys.latest) as string; const styleId = window.localStorage.getItem(storageKeys.latest) as string;
const styleItem = window.localStorage.getItem(styleKey(styleId)) const styleItem = window.localStorage.getItem(styleKey(styleId))
if(styleItem) return cb(JSON.parse(styleItem)) if (styleItem) {
loadDefaultStyle(cb) return JSON.parse(styleItem) as StyleSpecificationWithId;
}
return loadDefaultStyle();
} }
// Save current style replacing previous version // Save current style replacing previous version
save(mapStyle: StyleSpecification & { id: string }) { save(mapStyle: StyleSpecificationWithId) {
mapStyle = style.ensureStyleValidity(mapStyle) mapStyle = style.ensureStyleValidity(mapStyle)
const key = styleKey(mapStyle.id) const key = styleKey(mapStyle.id)

View File

@@ -1,6 +1,7 @@
import {derefLayers} from '@maplibre/maplibre-gl-style-spec' import {derefLayers} from '@maplibre/maplibre-gl-style-spec'
import type {StyleSpecification, LayerSpecification} from 'maplibre-gl' import type {StyleSpecification, LayerSpecification} from 'maplibre-gl'
import tokens from '../config/tokens.json' import tokens from '../config/tokens.json'
import type {StyleSpecificationWithId} from './definitions'
// Empty style is always used if no style could be restored or fetched // Empty style is always used if no style could be restored or fetched
const emptyStyle = ensureStyleValidity({ const emptyStyle = ensureStyleValidity({
@@ -13,15 +14,14 @@ function generateId() {
return Math.random().toString(36).substring(2, 9) 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) { if(!('id' in style) || !style.id) {
style.id = generateId(); 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 changedLayers = style.layers.map(layer => {
const changedLayer: LayerSpecification & { interactive?: any } = { ...layer } const changedLayer: LayerSpecification & { interactive?: any } = { ...layer }
delete changedLayer.interactive delete changedLayer.interactive
@@ -34,14 +34,14 @@ function ensureHasNoInteractive(style: StyleSpecification & {id: string}) {
} }
} }
function ensureHasNoRefs(style: StyleSpecification & {id: string}) { function ensureHasNoRefs(style: StyleSpecificationWithId) {
return { return {
...style, ...style,
layers: derefLayers(style.layers) layers: derefLayers(style.layers)
} }
} }
function ensureStyleValidity(style: StyleSpecification): StyleSpecification & { id: string } { function ensureStyleValidity(style: StyleSpecification): StyleSpecificationWithId {
return ensureHasNoInteractive(ensureHasNoRefs(ensureHasId(style))) return ensureHasNoInteractive(ensureHasNoRefs(ensureHasId(style)))
} }

View File

@@ -1,30 +1,27 @@
import style from './style' import style from './style'
import { StyleSpecificationWithId } from './definitions';
export function initialStyleUrl() { export function getStyleUrlFromAddressbarAndRemoveItIfNeeded(): string | null {
const initialUrl = new URL(window.location.href); 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<StyleSpecificationWithId> {
console.log('Loading style', styleUrl) console.log('Loading style', styleUrl)
fetch(styleUrl, { try {
mode: 'cors', const response = await fetch(styleUrl, {
credentials: "same-origin" mode: 'cors',
}) credentials: "same-origin"
.then(function(response) { });
return response.json(); const body = await response.json();
}) return style.ensureStyleValidity(body);
.then(function(body) { } catch {
cb(style.ensureStyleValidity(body)) console.warn('Could not fetch default style: ' + styleUrl)
}) return style.emptyStyle
.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())
} }

View File

@@ -3,7 +3,7 @@ import react from "@vitejs/plugin-react";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import istanbul from "vite-plugin-istanbul"; import istanbul from "vite-plugin-istanbul";
export default defineConfig({ export default defineConfig(({ mode }) => ({
server: { server: {
port: 8888, port: 8888,
}, },
@@ -27,7 +27,8 @@ export default defineConfig({
forceBuildInstrument: true, //Instrument the source code for cypress runs forceBuildInstrument: true, //Instrument the source code for cypress runs
}), }),
], ],
base: mode === "desktop" ? "/" : "/maputnik/",
define: { define: {
global: "window", global: "window"
}, },
}); }));