validate settings

This commit is contained in:
vcoppe
2026-02-01 18:45:40 +01:00
parent dba01e1826
commit bfd0d90abc
5 changed files with 168 additions and 38 deletions

View File

@@ -20,9 +20,8 @@
import { i18n } from '$lib/i18n.svelte'; import { i18n } from '$lib/i18n.svelte';
import { defaultBasemap, type CustomLayer } from '$lib/assets/layers'; import { defaultBasemap, type CustomLayer } from '$lib/assets/layers';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { customBasemapUpdate, isSelected, remove } from './utils'; import { remove } from './utils';
import { settings } from '$lib/logic/settings'; import { settings } from '$lib/logic/settings';
import { map } from '$lib/components/map/map';
import { dndzone } from 'svelte-dnd-action'; import { dndzone } from 'svelte-dnd-action';
const { const {
@@ -129,8 +128,8 @@
], ],
}; };
} }
$customLayers[layerId] = layer;
addLayer(layerId); addLayer(layerId);
$customLayers[layerId] = layer;
selectedLayerId = undefined; selectedLayerId = undefined;
setDataFromSelectedLayer(); setDataFromSelectedLayer();
} }
@@ -153,9 +152,7 @@
return $tree; return $tree;
}); });
if ($currentBasemap === layerId) { if ($currentBasemap !== layerId) {
$customBasemapUpdate++;
} else {
$currentBasemap = layerId; $currentBasemap = layerId;
} }
@@ -171,14 +168,6 @@
return $tree; 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) => { currentOverlays.update(($overlays) => {
if (!$overlays.overlays.hasOwnProperty('custom')) { if (!$overlays.overlays.hasOwnProperty('custom')) {
$overlays.overlays['custom'] = {}; $overlays.overlays['custom'] = {};

View File

@@ -167,11 +167,11 @@
{#if isSelected($selectedOverlayTree, selectedOverlay)} {#if isSelected($selectedOverlayTree, selectedOverlay)}
{#if $isLayerFromExtension(selectedOverlay)} {#if $isLayerFromExtension(selectedOverlay)}
{$getLayerName(selectedOverlay)} {$getLayerName(selectedOverlay)}
{:else if $customLayers.hasOwnProperty(selectedOverlay)}
{$customLayers[selectedOverlay].name}
{:else} {:else}
{i18n._(`layers.label.${selectedOverlay}`)} {i18n._(`layers.label.${selectedOverlay}`)}
{/if} {/if}
{:else if $customLayers.hasOwnProperty(selectedOverlay)}
{$customLayers[selectedOverlay].name}
{/if} {/if}
{/if} {/if}
</Select.Trigger> </Select.Trigger>

View File

@@ -76,5 +76,3 @@ export function removeAll(node: LayerTreeType, ids: string[]) {
}); });
return node; return node;
} }
export const customBasemapUpdate = writable(0);

View File

@@ -7,7 +7,7 @@ import {
overlays, overlays,
terrainSources, terrainSources,
} from '$lib/assets/layers'; } 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'; import { i18n } from '$lib/i18n.svelte';
const { currentBasemap, currentOverlays, customLayers, opacities, terrainSource } = settings; const { currentBasemap, currentOverlays, customLayers, opacities, terrainSource } = settings;
@@ -52,10 +52,10 @@ export class StyleManager {
} }
}); });
currentBasemap.subscribe(() => this.updateBasemap()); currentBasemap.subscribe(() => this.updateBasemap());
customBasemapUpdate.subscribe(() => this.updateBasemap());
currentOverlays.subscribe(() => this.updateOverlays()); currentOverlays.subscribe(() => this.updateOverlays());
opacities.subscribe(() => this.updateOverlays()); opacities.subscribe(() => this.updateOverlays());
terrainSource.subscribe(() => this.updateTerrain()); terrainSource.subscribe(() => this.updateTerrain());
customLayers.subscribe(() => this.updateBasemap());
} }
updateBasemap() { updateBasemap() {

View File

@@ -1,6 +1,7 @@
import { type Database } from '$lib/db'; import { type Database } from '$lib/db';
import { liveQuery } from 'dexie'; import { liveQuery } from 'dexie';
import { import {
basemaps,
defaultBasemap, defaultBasemap,
defaultBasemapTree, defaultBasemapTree,
defaultOpacities, defaultOpacities,
@@ -9,7 +10,10 @@ import {
defaultOverpassQueries, defaultOverpassQueries,
defaultOverpassTree, defaultOverpassTree,
defaultTerrainSource, defaultTerrainSource,
overlays,
overpassQueryData,
type CustomLayer, type CustomLayer,
type LayerTreeType,
} from '$lib/assets/layers'; } from '$lib/assets/layers';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { get, writable, type Writable } from 'svelte/store'; import { get, writable, type Writable } from 'svelte/store';
@@ -19,10 +23,12 @@ export class Setting<V> {
private _subscription: { unsubscribe: () => void } | null = null; private _subscription: { unsubscribe: () => void } | null = null;
private _key: string; private _key: string;
private _value: Writable<V>; private _value: Writable<V>;
private _validator?: (value: V) => V;
constructor(key: string, initial: V) { constructor(key: string, initial: V, validator?: (value: V) => V) {
this._key = key; this._key = key;
this._value = writable(initial); this._value = writable(initial);
this._validator = validator;
} }
connectToDatabase(db: Database) { connectToDatabase(db: Database) {
@@ -36,6 +42,9 @@ export class Setting<V> {
this._value.set(value); this._value.set(value);
} }
} else { } else {
if (this._validator) {
value = this._validator(value);
}
this._value.set(value); this._value.set(value);
} }
first = false; first = false;
@@ -73,11 +82,13 @@ export class SettingInitOnFirstRead<V> {
private _key: string; private _key: string;
private _value: Writable<V | undefined>; private _value: Writable<V | undefined>;
private _initial: V; 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._key = key;
this._value = writable(undefined); this._value = writable(undefined);
this._initial = initial; this._initial = initial;
this._validator = validator;
} }
connectToDatabase(db: Database) { connectToDatabase(db: Database) {
@@ -93,6 +104,9 @@ export class SettingInitOnFirstRead<V> {
this._value.set(value); this._value.set(value);
} }
} else { } else {
if (this._validator) {
value = this._validator(value);
}
this._value.set(value); this._value.set(value);
} }
first = false; first = false;
@@ -128,37 +142,166 @@ export class SettingInitOnFirstRead<V> {
} }
} }
function getValueValidator<V>(allowed: V[], fallback: V) {
const dict = new Set<V>(allowed);
return (value: V) => (dict.has(value) ? value : fallback);
}
function getArrayValidator<V>(allowed: V[]) {
const dict = new Set<V>(allowed);
return (value: V[]) => value.filter((v) => dict.has(v));
}
function getLayerValidator(allowed: Record<string, any>, fallback: string) {
return (layer: string) =>
allowed.hasOwnProperty(layer) ||
layer.startsWith('custom-') ||
layer.startsWith('extension-')
? layer
: fallback;
}
function filterLayerTree(t: LayerTreeType, allowed: Record<string, any>): 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<string, any>) {
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 = { export const settings = {
distanceUnits: new Setting<'metric' | 'imperial' | 'nautical'>('distanceUnits', 'metric'), distanceUnits: new Setting<DistanceUnits>(
velocityUnits: new Setting<'speed' | 'pace'>('velocityUnits', 'speed'), 'distanceUnits',
temperatureUnits: new Setting<'celsius' | 'fahrenheit'>('temperatureUnits', 'celsius'), 'metric',
getValueValidator<DistanceUnits>(['metric', 'imperial', 'nautical'], 'metric')
),
velocityUnits: new Setting<VelocityUnits>(
'velocityUnits',
'speed',
getValueValidator<VelocityUnits>(['speed', 'pace'], 'speed')
),
temperatureUnits: new Setting<TemperatureUnits>(
'temperatureUnits',
'celsius',
getValueValidator<TemperatureUnits>(['celsius', 'fahrenheit'], 'celsius')
),
elevationProfile: new Setting<boolean>('elevationProfile', true), elevationProfile: new Setting<boolean>('elevationProfile', true),
additionalDatasets: new Setting<string[]>('additionalDatasets', []), additionalDatasets: new Setting<AdditionalDataset[]>(
elevationFill: new Setting<'slope' | 'surface' | undefined>('elevationFill', undefined), 'additionalDatasets',
[],
getArrayValidator<AdditionalDataset>(['speed', 'hr', 'cad', 'atemp', 'power'])
),
elevationFill: new Setting<ElevationFill>(
'elevationFill',
undefined,
getValueValidator(['slope', 'surface', undefined], undefined)
),
treeFileView: new Setting<boolean>('fileView', false), treeFileView: new Setting<boolean>('fileView', false),
minimizeRoutingMenu: new Setting('minimizeRoutingMenu', false), minimizeRoutingMenu: new Setting('minimizeRoutingMenu', false),
routing: new Setting('routing', true), routing: new Setting('routing', true),
routingProfile: new Setting('routingProfile', 'bike'), routingProfile: new Setting<RoutingProfile>(
'routingProfile',
'bike',
getValueValidator<RoutingProfile>(
[
'bike',
'racing_bike',
'gravel_bike',
'mountain_bike',
'foot',
'motorcycle',
'water',
'railway',
],
'bike'
)
),
privateRoads: new Setting('privateRoads', false), privateRoads: new Setting('privateRoads', false),
currentBasemap: new Setting('currentBasemap', defaultBasemap), currentBasemap: new Setting(
previousBasemap: new Setting('previousBasemap', defaultBasemap), 'currentBasemap',
selectedBasemapTree: new Setting('selectedBasemapTree', defaultBasemapTree), defaultBasemap,
currentOverlays: new SettingInitOnFirstRead('currentOverlays', defaultOverlays), getLayerValidator(basemaps, defaultBasemap)
previousOverlays: new Setting('previousOverlays', defaultOverlays), ),
selectedOverlayTree: new Setting('selectedOverlayTree', defaultOverlayTree), 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: new SettingInitOnFirstRead(
'currentOverpassQueries', 'currentOverpassQueries',
defaultOverpassQueries defaultOverpassQueries,
getLayerTreeValidator(overpassQueryData)
),
selectedOverpassTree: new Setting(
'selectedOverpassTree',
defaultOverpassTree,
getLayerTreeValidator(overpassQueryData)
), ),
selectedOverpassTree: new Setting('selectedOverpassTree', defaultOverpassTree),
opacities: new Setting('opacities', defaultOpacities), opacities: new Setting('opacities', defaultOpacities),
customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}), customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}),
customBasemapOrder: new Setting<string[]>('customBasemapOrder', []), customBasemapOrder: new Setting<string[]>('customBasemapOrder', []),
customOverlayOrder: new Setting<string[]>('customOverlayOrder', []), customOverlayOrder: new Setting<string[]>('customOverlayOrder', []),
terrainSource: new Setting('terrainSource', defaultTerrainSource), terrainSource: new Setting<TerrainSource>(
'terrainSource',
defaultTerrainSource,
getValueValidator(['maptiler-dem', 'mapterhorn'], defaultTerrainSource)
),
directionMarkers: new Setting('directionMarkers', false), directionMarkers: new Setting('directionMarkers', false),
distanceMarkers: new Setting('distanceMarkers', false), distanceMarkers: new Setting('distanceMarkers', false),
streetViewSource: new Setting('streetViewSource', 'mapillary'), streetViewSource: new Setting<StreetViewSource>(
'streetViewSource',
'mapillary',
getValueValidator<StreetViewSource>(['mapillary', 'google'], 'mapillary')
),
fileOrder: new Setting<string[]>('fileOrder', []), fileOrder: new Setting<string[]>('fileOrder', []),
defaultOpacity: new Setting('defaultOpacity', 0.7), defaultOpacity: new Setting('defaultOpacity', 0.7),
defaultWidth: new Setting('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5), defaultWidth: new Setting('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),