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
+88
View File
@@ -0,0 +1,88 @@
import style from '../style'
import {format} from '@maplibre/maplibre-gl-style-spec'
import ReconnectingWebSocket from 'reconnecting-websocket'
import type {IStyleStore, OnStyleChangedCallback, StyleSpecificationWithId} from '../definitions'
export type ApiStyleStoreOptions = {
onLocalStyleChange?: OnStyleChangedCallback
}
export class ApiStyleStore implements IStyleStore {
localUrl: string;
websocketUrl: string;
latestStyleId: string | undefined = undefined;
onLocalStyleChange: OnStyleChangedCallback;
constructor(opts: ApiStyleStoreOptions) {
this.onLocalStyleChange = opts.onLocalStyleChange || (() => {})
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)
}
async init(): Promise<void> {
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() {
const connection = new ReconnectingWebSocket(this.websocketUrl)
connection.onmessage = e => {
if(!e.data) return
console.log('Received style update from API')
let parsedStyle = style.emptyStyle
try {
parsedStyle = JSON.parse(e.data)
} catch(err) {
console.error(err)
}
const updatedStyle = style.ensureStyleValidity(parsedStyle)
this.onLocalStyleChange(updatedStyle)
}
}
async getLatestStyle(): Promise<StyleSpecificationWithId> {
if(this.latestStyleId) {
const response = await fetch(this.localUrl + '/styles/' + this.latestStyleId, {
mode: 'cors',
});
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: StyleSpecificationWithId) {
const styleJSON = format(
style.stripAccessTokens(
style.replaceAccessTokens(mapStyle)
)
);
const id = mapStyle.id
fetch(this.localUrl + '/styles/' + id, {
method: "PUT",
mode: 'cors',
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: styleJSON
})
.catch(function(error) {
if(error) console.error(error)
})
return mapStyle
}
}
+29
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 };
+118
View File
@@ -0,0 +1,118 @@
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'
const storageKeys = {
latest: [storagePrefix, 'latest_style'].join(':'),
accessToken: [storagePrefix, 'access_token'].join(':')
}
const defaultStyleUrl = publicSources[0].url
// Fetch a default style via URL and return it or a fallback style via callback
export function loadDefaultStyle(): Promise<StyleSpecificationWithId> {
return loadStyleUrl(defaultStyleUrl);
}
// Return style ids and dates of all styles stored in local storage
function loadStoredStyles() {
const styles = []
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i)
if(isStyleKey(key!)) {
styles.push(fromKey(key!))
}
}
return styles
}
function isStyleKey(key: string) {
const parts = key.split(":")
return parts.length === 3 && parts[0] === storagePrefix && parts[1] === stylePrefix
}
// Load style id from key
function fromKey(key: string) {
if(!isStyleKey(key)) {
throw "Key is not a valid style key"
}
const parts = key.split(":")
const styleId = parts[2]
return styleId
}
// Calculate key that identifies the style with a version
function styleKey(styleId: string) {
return [storagePrefix, stylePrefix, styleId].join(":")
}
// Manages many possible styles that are stored in the local storage
export class StyleStore implements IStyleStore {
/**
* List of style ids
*/
mapStyles: string[];
// Tile store will load all items from local storage and
// assume they do not change will working on it
constructor() {
this.mapStyles = loadStoredStyles();
}
// Delete entire style history
purge() {
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i) as string;
if(key.startsWith(storagePrefix)) {
window.localStorage.removeItem(key)
}
}
}
// Find the last edited style
async getLatestStyle(): Promise<StyleSpecificationWithId> {
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 JSON.parse(styleItem) as StyleSpecificationWithId;
}
return loadDefaultStyle();
}
// Save current style replacing previous version
save(mapStyle: StyleSpecificationWithId) {
mapStyle = style.ensureStyleValidity(mapStyle)
const key = styleKey(mapStyle.id)
const saveFn = () => {
window.localStorage.setItem(key, JSON.stringify(mapStyle))
window.localStorage.setItem(storageKeys.latest, mapStyle.id)
}
try {
saveFn()
} catch (e) {
// Handle quota exceeded error
if (e instanceof DOMException && (
e.code === 22 || // Firefox
e.code === 1014 || // Firefox
e.name === 'QuotaExceededError' ||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED'
)) {
this.purge()
saveFn() // Retry after clearing
} else {
throw e
}
}
return mapStyle
}
}