Files
gpx.studio/website/src/lib/logic/settings.ts

335 lines
10 KiB
TypeScript
Raw Normal View History

2025-11-09 18:03:27 +01:00
import { type Database } from '$lib/db';
2025-06-21 21:07:36 +02:00
import { liveQuery } from 'dexie';
import {
2026-02-01 18:45:40 +01:00
basemaps,
2025-06-21 21:07:36 +02:00
defaultBasemap,
defaultBasemapTree,
defaultOpacities,
defaultOverlays,
defaultOverlayTree,
defaultOverpassQueries,
defaultOverpassTree,
defaultTerrainSource,
2026-02-01 18:45:40 +01:00
overlays,
overpassQueryData,
2025-06-21 21:07:36 +02:00
type CustomLayer,
2026-02-01 18:45:40 +01:00
type LayerTreeType,
2025-06-21 21:07:36 +02:00
} from '$lib/assets/layers';
import { browser } from '$app/environment';
2025-10-17 23:54:45 +02:00
import { get, writable, type Writable } from 'svelte/store';
2025-06-21 21:07:36 +02:00
export class Setting<V> {
2025-11-09 18:03:27 +01:00
private _db: Database | null = null;
private _subscription: { unsubscribe: () => void } | null = null;
2025-06-21 21:07:36 +02:00
private _key: string;
2025-10-17 23:54:45 +02:00
private _value: Writable<V>;
2026-02-01 18:45:40 +01:00
private _validator?: (value: V) => V;
2025-06-21 21:07:36 +02:00
2026-02-01 18:45:40 +01:00
constructor(key: string, initial: V, validator?: (value: V) => V) {
2025-06-21 21:07:36 +02:00
this._key = key;
2025-10-17 23:54:45 +02:00
this._value = writable(initial);
2026-02-01 18:45:40 +01:00
this._validator = validator;
2025-11-09 18:03:27 +01:00
}
connectToDatabase(db: Database) {
if (this._db) return;
this._db = db;
2025-06-21 21:07:36 +02:00
let first = true;
2025-11-09 18:03:27 +01:00
this._subscription = liveQuery(() => db.settings.get(this._key)).subscribe((value) => {
2025-06-21 21:07:36 +02:00
if (value === undefined) {
if (!first) {
2025-10-17 23:54:45 +02:00
this._value.set(value);
2025-06-21 21:07:36 +02:00
}
} else {
2026-02-01 18:45:40 +01:00
if (this._validator) {
value = this._validator(value);
}
2025-10-17 23:54:45 +02:00
this._value.set(value);
2025-06-21 21:07:36 +02:00
}
first = false;
});
}
2025-11-09 18:03:27 +01:00
disconnectFromDatabase() {
this._subscription?.unsubscribe();
this._subscription = null;
this._db = null;
}
2025-10-17 23:54:45 +02:00
subscribe(run: (value: V) => void, invalidate?: (value?: V) => void) {
return this._value.subscribe(run, invalidate);
2025-06-21 21:07:36 +02:00
}
2025-11-09 18:03:27 +01:00
set(value: V) {
if (typeof value === 'object' || value !== get(this._value)) {
if (this._db) {
this._db.settings.put(value, this._key);
} else {
this._value.set(value);
}
2025-10-17 23:54:45 +02:00
}
}
update(callback: (value: any) => any) {
2025-11-09 18:03:27 +01:00
this.set(callback(get(this._value)));
2025-06-21 21:07:36 +02:00
}
}
export class SettingInitOnFirstRead<V> {
2025-11-09 18:03:27 +01:00
private _db: Database | null = null;
private _subscription: { unsubscribe: () => void } | null = null;
2025-06-21 21:07:36 +02:00
private _key: string;
2025-10-17 23:54:45 +02:00
private _value: Writable<V | undefined>;
2025-11-09 18:03:27 +01:00
private _initial: V;
2026-02-01 18:45:40 +01:00
private _validator?: (value: V) => V;
2025-06-21 21:07:36 +02:00
2026-02-01 18:45:40 +01:00
constructor(key: string, initial: V, validator?: (value: V) => V) {
2025-06-21 21:07:36 +02:00
this._key = key;
2025-10-17 23:54:45 +02:00
this._value = writable(undefined);
2025-11-09 18:03:27 +01:00
this._initial = initial;
2026-02-01 18:45:40 +01:00
this._validator = validator;
2025-11-09 18:03:27 +01:00
}
connectToDatabase(db: Database) {
if (this._db) return;
this._db = db;
2025-06-21 21:07:36 +02:00
let first = true;
2025-11-09 18:03:27 +01:00
this._subscription = liveQuery(() => db.settings.get(this._key)).subscribe((value) => {
2025-06-21 21:07:36 +02:00
if (value === undefined) {
if (first) {
2025-11-09 18:03:27 +01:00
this._value.set(this._initial);
2025-06-21 21:07:36 +02:00
} else {
2025-10-17 23:54:45 +02:00
this._value.set(value);
2025-06-21 21:07:36 +02:00
}
} else {
2026-02-01 18:45:40 +01:00
if (this._validator) {
value = this._validator(value);
}
2025-10-17 23:54:45 +02:00
this._value.set(value);
2025-06-21 21:07:36 +02:00
}
first = false;
});
}
2025-11-09 18:45:20 +01:00
initialize() {
this.set(this._initial);
}
2025-11-09 18:03:27 +01:00
disconnectFromDatabase() {
this._subscription?.unsubscribe();
this._subscription = null;
this._db = null;
}
2025-10-17 23:54:45 +02:00
subscribe(run: (value: V | undefined) => void, invalidate?: (value?: V | undefined) => void) {
return this._value.subscribe(run, invalidate);
}
2025-11-09 18:03:27 +01:00
set(value: V) {
if (typeof value === 'object' || value !== get(this._value)) {
if (this._db) {
this._db.settings.put(value, this._key);
} else {
this._value.set(value);
}
2025-10-17 23:54:45 +02:00
}
2025-06-21 21:07:36 +02:00
}
2025-10-17 23:54:45 +02:00
update(callback: (value: any) => any) {
2025-11-09 18:03:27 +01:00
this.set(callback(get(this._value)));
2025-06-21 21:07:36 +02:00
}
}
2026-02-01 18:45:40 +01:00
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';
2025-06-21 21:07:36 +02:00
export const settings = {
2026-02-01 18:45:40 +01:00
distanceUnits: new Setting<DistanceUnits>(
'distanceUnits',
'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')
),
2025-11-09 18:03:27 +01:00
elevationProfile: new Setting<boolean>('elevationProfile', true),
2026-02-01 18:45:40 +01:00
additionalDatasets: new Setting<AdditionalDataset[]>(
'additionalDatasets',
[],
getArrayValidator<AdditionalDataset>(['speed', 'hr', 'cad', 'atemp', 'power'])
),
elevationFill: new Setting<ElevationFill>(
'elevationFill',
undefined,
getValueValidator(['slope', 'surface', undefined], undefined)
),
2025-11-09 18:03:27 +01:00
treeFileView: new Setting<boolean>('fileView', false),
minimizeRoutingMenu: new Setting('minimizeRoutingMenu', false),
routing: new Setting('routing', true),
2026-02-01 18:45:40 +01:00
routingProfile: new Setting<RoutingProfile>(
'routingProfile',
'bike',
getValueValidator<RoutingProfile>(
[
'bike',
'racing_bike',
'gravel_bike',
'mountain_bike',
'foot',
'motorcycle',
'water',
'railway',
],
'bike'
)
),
2025-11-09 18:03:27 +01:00
privateRoads: new Setting('privateRoads', false),
2026-02-01 18:45:40 +01:00
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)
),
2025-06-21 21:07:36 +02:00
currentOverpassQueries: new SettingInitOnFirstRead(
'currentOverpassQueries',
2026-02-01 18:45:40 +01:00
defaultOverpassQueries,
getLayerTreeValidator(overpassQueryData)
),
selectedOverpassTree: new Setting(
'selectedOverpassTree',
defaultOverpassTree,
getLayerTreeValidator(overpassQueryData)
2025-06-21 21:07:36 +02:00
),
2025-11-09 18:03:27 +01:00
opacities: new Setting('opacities', defaultOpacities),
customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}),
customBasemapOrder: new Setting<string[]>('customBasemapOrder', []),
customOverlayOrder: new Setting<string[]>('customOverlayOrder', []),
2026-02-01 18:45:40 +01:00
terrainSource: new Setting<TerrainSource>(
'terrainSource',
defaultTerrainSource,
getValueValidator(['maptiler-dem', 'mapterhorn'], defaultTerrainSource)
),
2025-11-09 18:03:27 +01:00
directionMarkers: new Setting('directionMarkers', false),
distanceMarkers: new Setting('distanceMarkers', false),
2026-02-01 18:45:40 +01:00
streetViewSource: new Setting<StreetViewSource>(
'streetViewSource',
'mapillary',
getValueValidator<StreetViewSource>(['mapillary', 'google'], 'mapillary')
),
2025-11-09 18:03:27 +01:00
fileOrder: new Setting<string[]>('fileOrder', []),
defaultOpacity: new Setting('defaultOpacity', 0.7),
defaultWidth: new Setting('defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),
bottomPanelSize: new Setting('bottomPanelSize', 170),
rightPanelSize: new Setting('rightPanelSize', 240),
connectToDatabase(db: Database) {
for (const key in settings) {
const setting = (settings as any)[key];
if (setting instanceof Setting || setting instanceof SettingInitOnFirstRead) {
setting.connectToDatabase(db);
}
}
},
disconnectFromDatabase() {
for (const key in settings) {
const setting = (settings as any)[key];
if (setting instanceof Setting || setting instanceof SettingInitOnFirstRead) {
setting.disconnectFromDatabase();
}
}
},
2025-11-09 18:45:20 +01:00
initialize() {
for (const key in settings) {
const setting = (settings as any)[key];
if (setting instanceof SettingInitOnFirstRead) {
setting.initialize();
}
}
},
2025-06-21 21:07:36 +02:00
};