From bfd0d90abc7e3122bda4238e54e9b0da6a4ed196 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Sun, 1 Feb 2026 18:45:40 +0100 Subject: [PATCH] validate settings --- .../map/layer-control/CustomLayers.svelte | 17 +- .../layer-control/LayerControlSettings.svelte | 4 +- .../lib/components/map/layer-control/utils.ts | 2 - website/src/lib/components/map/style.ts | 4 +- website/src/lib/logic/settings.ts | 179 ++++++++++++++++-- 5 files changed, 168 insertions(+), 38 deletions(-) diff --git a/website/src/lib/components/map/layer-control/CustomLayers.svelte b/website/src/lib/components/map/layer-control/CustomLayers.svelte index e8214c807..9789b67ad 100644 --- a/website/src/lib/components/map/layer-control/CustomLayers.svelte +++ b/website/src/lib/components/map/layer-control/CustomLayers.svelte @@ -20,9 +20,8 @@ import { i18n } from '$lib/i18n.svelte'; import { defaultBasemap, type CustomLayer } from '$lib/assets/layers'; import { onMount } from 'svelte'; - import { customBasemapUpdate, isSelected, remove } from './utils'; + import { remove } from './utils'; import { settings } from '$lib/logic/settings'; - import { map } from '$lib/components/map/map'; import { dndzone } from 'svelte-dnd-action'; const { @@ -129,8 +128,8 @@ ], }; } - $customLayers[layerId] = layer; addLayer(layerId); + $customLayers[layerId] = layer; selectedLayerId = undefined; setDataFromSelectedLayer(); } @@ -153,9 +152,7 @@ return $tree; }); - if ($currentBasemap === layerId) { - $customBasemapUpdate++; - } else { + if ($currentBasemap !== layerId) { $currentBasemap = layerId; } @@ -171,14 +168,6 @@ return $tree; }); - if ($map && $currentOverlays && isSelected($currentOverlays, layerId)) { - try { - $map.removeImport(layerId); - } catch (e) { - // No reliable way to check if the map is ready to remove sources and layers - } - } - currentOverlays.update(($overlays) => { if (!$overlays.overlays.hasOwnProperty('custom')) { $overlays.overlays['custom'] = {}; diff --git a/website/src/lib/components/map/layer-control/LayerControlSettings.svelte b/website/src/lib/components/map/layer-control/LayerControlSettings.svelte index 15f377379..4dc0e2efc 100644 --- a/website/src/lib/components/map/layer-control/LayerControlSettings.svelte +++ b/website/src/lib/components/map/layer-control/LayerControlSettings.svelte @@ -167,11 +167,11 @@ {#if isSelected($selectedOverlayTree, selectedOverlay)} {#if $isLayerFromExtension(selectedOverlay)} {$getLayerName(selectedOverlay)} + {:else if $customLayers.hasOwnProperty(selectedOverlay)} + {$customLayers[selectedOverlay].name} {:else} {i18n._(`layers.label.${selectedOverlay}`)} {/if} - {:else if $customLayers.hasOwnProperty(selectedOverlay)} - {$customLayers[selectedOverlay].name} {/if} {/if} diff --git a/website/src/lib/components/map/layer-control/utils.ts b/website/src/lib/components/map/layer-control/utils.ts index 6f173ff60..fdfd1c872 100644 --- a/website/src/lib/components/map/layer-control/utils.ts +++ b/website/src/lib/components/map/layer-control/utils.ts @@ -76,5 +76,3 @@ export function removeAll(node: LayerTreeType, ids: string[]) { }); return node; } - -export const customBasemapUpdate = writable(0); diff --git a/website/src/lib/components/map/style.ts b/website/src/lib/components/map/style.ts index 274ebad6d..399a1b02d 100644 --- a/website/src/lib/components/map/style.ts +++ b/website/src/lib/components/map/style.ts @@ -7,7 +7,7 @@ import { overlays, terrainSources, } from '$lib/assets/layers'; -import { customBasemapUpdate, getLayers } from '$lib/components/map/layer-control/utils'; +import { getLayers } from '$lib/components/map/layer-control/utils'; import { i18n } from '$lib/i18n.svelte'; const { currentBasemap, currentOverlays, customLayers, opacities, terrainSource } = settings; @@ -52,10 +52,10 @@ export class StyleManager { } }); currentBasemap.subscribe(() => this.updateBasemap()); - customBasemapUpdate.subscribe(() => this.updateBasemap()); currentOverlays.subscribe(() => this.updateOverlays()); opacities.subscribe(() => this.updateOverlays()); terrainSource.subscribe(() => this.updateTerrain()); + customLayers.subscribe(() => this.updateBasemap()); } updateBasemap() { diff --git a/website/src/lib/logic/settings.ts b/website/src/lib/logic/settings.ts index c86df6ac0..3ec53e5c9 100644 --- a/website/src/lib/logic/settings.ts +++ b/website/src/lib/logic/settings.ts @@ -1,6 +1,7 @@ import { type Database } from '$lib/db'; import { liveQuery } from 'dexie'; import { + basemaps, defaultBasemap, defaultBasemapTree, defaultOpacities, @@ -9,7 +10,10 @@ import { defaultOverpassQueries, defaultOverpassTree, defaultTerrainSource, + overlays, + overpassQueryData, type CustomLayer, + type LayerTreeType, } from '$lib/assets/layers'; import { browser } from '$app/environment'; import { get, writable, type Writable } from 'svelte/store'; @@ -19,10 +23,12 @@ export class Setting { private _subscription: { unsubscribe: () => void } | null = null; private _key: string; private _value: Writable; + private _validator?: (value: V) => V; - constructor(key: string, initial: V) { + constructor(key: string, initial: V, validator?: (value: V) => V) { this._key = key; this._value = writable(initial); + this._validator = validator; } connectToDatabase(db: Database) { @@ -36,6 +42,9 @@ export class Setting { this._value.set(value); } } else { + if (this._validator) { + value = this._validator(value); + } this._value.set(value); } first = false; @@ -73,11 +82,13 @@ export class SettingInitOnFirstRead { private _key: string; private _value: Writable; private _initial: V; + private _validator?: (value: V) => V; - constructor(key: string, initial: V) { + constructor(key: string, initial: V, validator?: (value: V) => V) { this._key = key; this._value = writable(undefined); this._initial = initial; + this._validator = validator; } connectToDatabase(db: Database) { @@ -93,6 +104,9 @@ export class SettingInitOnFirstRead { this._value.set(value); } } else { + if (this._validator) { + value = this._validator(value); + } this._value.set(value); } first = false; @@ -128,37 +142,166 @@ export class SettingInitOnFirstRead { } } +function getValueValidator(allowed: V[], fallback: V) { + const dict = new Set(allowed); + return (value: V) => (dict.has(value) ? value : fallback); +} + +function getArrayValidator(allowed: V[]) { + const dict = new Set(allowed); + return (value: V[]) => value.filter((v) => dict.has(v)); +} + +function getLayerValidator(allowed: Record, fallback: string) { + return (layer: string) => + allowed.hasOwnProperty(layer) || + layer.startsWith('custom-') || + layer.startsWith('extension-') + ? layer + : fallback; +} + +function filterLayerTree(t: LayerTreeType, allowed: Record): LayerTreeType { + const filtered: LayerTreeType = {}; + Object.entries(t).forEach(([key, value]) => { + if (typeof value === 'object') { + filtered[key] = filterLayerTree(value, allowed); + } else if ( + allowed.hasOwnProperty(key) || + key.startsWith('custom-') || + key.startsWith('extension-') + ) { + filtered[key] = value; + } + }); + return filtered; +} + +function getLayerTreeValidator(allowed: Record) { + return (value: LayerTreeType) => filterLayerTree(value, allowed); +} + +type DistanceUnits = 'metric' | 'imperial' | 'nautical'; +type VelocityUnits = 'speed' | 'pace'; +type TemperatureUnits = 'celsius' | 'fahrenheit'; +type AdditionalDataset = 'speed' | 'hr' | 'cad' | 'atemp' | 'power'; +type ElevationFill = 'slope' | 'surface' | undefined; +type RoutingProfile = + | 'bike' + | 'racing_bike' + | 'gravel_bike' + | 'mountain_bike' + | 'foot' + | 'motorcycle' + | 'water' + | 'railway'; +type TerrainSource = 'maptiler-dem' | 'mapterhorn'; +type StreetViewSource = 'mapillary' | 'google'; + export const settings = { - distanceUnits: new Setting<'metric' | 'imperial' | 'nautical'>('distanceUnits', 'metric'), - velocityUnits: new Setting<'speed' | 'pace'>('velocityUnits', 'speed'), - temperatureUnits: new Setting<'celsius' | 'fahrenheit'>('temperatureUnits', 'celsius'), + distanceUnits: new Setting( + 'distanceUnits', + 'metric', + getValueValidator(['metric', 'imperial', 'nautical'], 'metric') + ), + velocityUnits: new Setting( + 'velocityUnits', + 'speed', + getValueValidator(['speed', 'pace'], 'speed') + ), + temperatureUnits: new Setting( + 'temperatureUnits', + 'celsius', + getValueValidator(['celsius', 'fahrenheit'], 'celsius') + ), elevationProfile: new Setting('elevationProfile', true), - additionalDatasets: new Setting('additionalDatasets', []), - elevationFill: new Setting<'slope' | 'surface' | undefined>('elevationFill', undefined), + additionalDatasets: new Setting( + 'additionalDatasets', + [], + getArrayValidator(['speed', 'hr', 'cad', 'atemp', 'power']) + ), + elevationFill: new Setting( + 'elevationFill', + undefined, + getValueValidator(['slope', 'surface', undefined], undefined) + ), treeFileView: new Setting('fileView', false), minimizeRoutingMenu: new Setting('minimizeRoutingMenu', false), routing: new Setting('routing', true), - routingProfile: new Setting('routingProfile', 'bike'), + routingProfile: new Setting( + 'routingProfile', + 'bike', + getValueValidator( + [ + 'bike', + 'racing_bike', + 'gravel_bike', + 'mountain_bike', + 'foot', + 'motorcycle', + 'water', + 'railway', + ], + 'bike' + ) + ), privateRoads: new Setting('privateRoads', false), - currentBasemap: new Setting('currentBasemap', defaultBasemap), - previousBasemap: new Setting('previousBasemap', defaultBasemap), - selectedBasemapTree: new Setting('selectedBasemapTree', defaultBasemapTree), - currentOverlays: new SettingInitOnFirstRead('currentOverlays', defaultOverlays), - previousOverlays: new Setting('previousOverlays', defaultOverlays), - selectedOverlayTree: new Setting('selectedOverlayTree', defaultOverlayTree), + currentBasemap: new Setting( + 'currentBasemap', + defaultBasemap, + getLayerValidator(basemaps, defaultBasemap) + ), + previousBasemap: new Setting( + 'previousBasemap', + defaultBasemap, + getLayerValidator(Object.keys(basemaps), defaultBasemap) + ), + selectedBasemapTree: new Setting( + 'selectedBasemapTree', + defaultBasemapTree, + getLayerTreeValidator(basemaps) + ), + currentOverlays: new SettingInitOnFirstRead( + 'currentOverlays', + defaultOverlays, + getLayerTreeValidator(overlays) + ), + previousOverlays: new Setting( + 'previousOverlays', + defaultOverlays, + getLayerTreeValidator(overlays) + ), + selectedOverlayTree: new Setting( + 'selectedOverlayTree', + defaultOverlayTree, + getLayerTreeValidator(overlays) + ), currentOverpassQueries: new SettingInitOnFirstRead( 'currentOverpassQueries', - defaultOverpassQueries + defaultOverpassQueries, + getLayerTreeValidator(overpassQueryData) + ), + selectedOverpassTree: new Setting( + 'selectedOverpassTree', + defaultOverpassTree, + getLayerTreeValidator(overpassQueryData) ), - selectedOverpassTree: new Setting('selectedOverpassTree', defaultOverpassTree), opacities: new Setting('opacities', defaultOpacities), customLayers: new Setting>('customLayers', {}), customBasemapOrder: new Setting('customBasemapOrder', []), customOverlayOrder: new Setting('customOverlayOrder', []), - terrainSource: new Setting('terrainSource', defaultTerrainSource), + terrainSource: new Setting( + 'terrainSource', + defaultTerrainSource, + getValueValidator(['maptiler-dem', 'mapterhorn'], defaultTerrainSource) + ), directionMarkers: new Setting('directionMarkers', false), distanceMarkers: new Setting('distanceMarkers', false), - streetViewSource: new Setting('streetViewSource', 'mapillary'), + streetViewSource: new Setting( + 'streetViewSource', + 'mapillary', + getValueValidator(['mapillary', 'google'], 'mapillary') + ), fileOrder: new Setting('fileOrder', []), defaultOpacity: new Setting('defaultOpacity', 0.7), defaultWidth: new Setting('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),