refactoring

This commit is contained in:
vcoppe committed 2026-10-11 11:18:36 +02:00
1 parent 4795513344
commit 890bcd765a
169 files changed
+263 -256

No files matched your search

+149
View File
@@ -0,0 +1,149 @@
import { get } from 'svelte/store';
import maplibregl from 'maplibre-gl';
import { map } from '$lib/components/map/map';
import { page } from '$app/state';
import { engine, FileStateCollectionObserver, type FileState } from '$lib/engine';
const { statistics } = engine;
export class BoundsManager {
private _bounds: maplibregl.LngLatBounds = new maplibregl.LngLatBounds();
private _files: Set<string> = new Set();
private _fileStateCollectionObserver: FileStateCollectionObserver | null = null;
private _unsubscribes: (() => void)[] = [];
constructor() {
this._fileStateCollectionObserver = new FileStateCollectionObserver(
(newFiles) => {
if (page.url.hash.length == 0) {
this.fitBoundsOnLoad(Array.from(newFiles.keys()));
}
},
(fileId) => {},
() => {}
);
}
fitBoundsOnLoad(files: string[]) {
this.reset();
this._files = new Set(files);
this._fileStateCollectionObserver = new FileStateCollectionObserver(
(newFiles) => {
newFiles.forEach((fileState, fileId) => {
if (this._files.has(fileId)) {
this._unsubscribes.push(
fileState.subscribe((state) => {
this.addBoundsFromFile(fileId, state);
})
);
}
});
},
(fileId) => {},
() => {}
);
}
addBoundsFromFile(fileId: string, file: FileState) {
if (!file || !this._files.has(fileId)) return;
this._files.delete(fileId);
if (file.statistics.bounds) {
let bounds = new maplibregl.LngLatBounds([
file.statistics.bounds.west,
file.statistics.bounds.south,
file.statistics.bounds.east,
file.statistics.bounds.north,
]);
if (!this.validBounds(bounds)) return;
this._bounds.extend(bounds);
}
if (this._files.size === 0) {
this.finalizeFitBounds();
}
}
finalizeFitBounds() {
if (
this._bounds.getSouth() >= this._bounds.getNorth() &&
this._bounds.getWest() >= this._bounds.getEast()
) {
return;
}
this._unsubscribes.push(
map.subscribe((map_) => {
if (!map_) return;
map_.fitBounds(this._bounds, { padding: 80, linear: true, animate: false });
this.reset();
})
);
}
reset() {
if (this._fileStateCollectionObserver) {
this._fileStateCollectionObserver.destroy();
}
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
this._unsubscribes = [];
this._bounds = new maplibregl.LngLatBounds([180, 90, -180, -90]);
}
centerMapOnSelection() {
const bounds = new maplibregl.LngLatBounds([180, 90, -180, -90]);
const stats = get(statistics);
if (stats.global.bounds) {
bounds.extend([
[stats.global.bounds.west, stats.global.bounds.south],
[stats.global.bounds.east, stats.global.bounds.north],
]);
}
// the waypoints are not part of the statistics of the selection
const selection = get(engine.selection);
let waypointIds: Set<string> | undefined; // all the waypoints of the file if undefined
let fileIds: string[] = [];
switch (selection.type) {
case 'file':
fileIds = selection.fileIds;
break;
case 'waypoints':
fileIds = [selection.fileId];
break;
case 'waypoint':
fileIds = [selection.fileId];
waypointIds = new Set(selection.waypointIds);
break;
}
const files = get(engine.files);
for (const fileId of fileIds) {
const file = files.get(fileId);
if (!file) continue;
for (const feature of get(file).waypoints.features) {
if (waypointIds && !waypointIds.has(feature.properties.waypointId)) continue;
bounds.extend(feature.geometry.coordinates as [number, number]);
}
}
if (!this.validBounds(bounds)) return;
get(map)?.fitBounds(bounds, {
padding: 80,
easing: () => 1,
maxZoom: 15,
});
}
validBounds(bounds: maplibregl.LngLatBounds) {
return (
bounds.getSouthWest().lat !== 90 ||
bounds.getSouthWest().lng !== 180 ||
bounds.getNorthEast().lat !== -90 ||
bounds.getNorthEast().lng !== -180
);
}
}
export const boundsManager = new BoundsManager();
+61
View File
@@ -0,0 +1,61 @@
import { map } from '$lib/components/map/map';
import { get, writable, type Writable } from 'svelte/store';
export enum MapCursorState {
DEFAULT,
LAYER_HOVER,
TOOL_WITH_CROSSHAIR,
WAYPOINT_HOVER,
WAYPOINT_DRAGGING,
ANCHOR_HOVER,
ANCHOR_DRAGGING,
SCISSORS,
SPLIT_CONTROL,
MAPILLARY_HOVER,
STREET_VIEW_CROSSHAIR,
}
const scissorsCursor = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1"><path d="M 3.200 3.200 C 0.441 5.959, 2.384 9.516, 7 10.154 C 10.466 10.634, 10.187 13.359, 6.607 13.990 C 2.934 14.637, 1.078 17.314, 2.612 19.750 C 4.899 23.380, 10 21.935, 10 17.657 C 10 16.445, 12.405 13.128, 15.693 9.805 C 18.824 6.641, 21.066 3.732, 20.674 3.341 C 20.283 2.950, 18.212 4.340, 16.072 6.430 C 12.019 10.388, 10 10.458, 10 6.641 C 10 2.602, 5.882 0.518, 3.200 3.200 M 4.446 5.087 C 3.416 6.755, 5.733 8.667, 7.113 7.287 C 8.267 6.133, 7.545 4, 6 4 C 5.515 4, 4.816 4.489, 4.446 5.087 M 14 14.813 C 14 16.187, 19.935 21.398, 20.667 20.667 C 21.045 20.289, 20.065 18.634, 18.490 16.990 C 15.661 14.036, 14 13.231, 14 14.813 M 4.446 17.087 C 3.416 18.755, 5.733 20.667, 7.113 19.287 C 8.267 18.133, 7.545 16, 6 16 C 5.515 16, 4.816 16.489, 4.446 17.087" stroke="black" stroke-width="1.2" fill="white" fill-rule="evenodd"/></svg>') 12 12, auto`;
const cursorStyles = {
[MapCursorState.DEFAULT]: 'default',
[MapCursorState.LAYER_HOVER]: 'pointer',
[MapCursorState.WAYPOINT_HOVER]: 'pointer',
[MapCursorState.WAYPOINT_DRAGGING]: 'grabbing',
[MapCursorState.ANCHOR_HOVER]: 'pointer',
[MapCursorState.ANCHOR_DRAGGING]: 'grabbing',
[MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair',
[MapCursorState.SCISSORS]: scissorsCursor,
[MapCursorState.SPLIT_CONTROL]: 'pointer',
[MapCursorState.MAPILLARY_HOVER]: 'pointer',
[MapCursorState.STREET_VIEW_CROSSHAIR]: 'crosshair',
};
export class MapCursor {
private _states: Writable<Set<MapCursorState>>;
constructor() {
this._states = writable(new Set());
this._states.subscribe((states) => {
let state = Array.from(states.values()).reduce((max, value) => {
return value > max ? value : max;
}, MapCursorState.DEFAULT);
let canvas = get(map)?.getCanvas();
if (canvas) {
canvas.style.cursor = cursorStyles[state];
}
});
}
notify(cursorState: MapCursorState, isActive: boolean) {
this._states.update((states) => {
if (isActive) {
states.add(cursorState);
} else {
states.delete(cursorState);
}
return states;
});
}
}
export const mapCursor = new MapCursor();
@@ -0,0 +1,20 @@
import { writable, type Writable } from 'svelte/store';
import { engine, type GlobalStatistics } from '$lib/engine';
import type { Coordinates } from '$lib/geo/coordinates';
/** The part of the selection that is highlighted in the elevation profile. */
export type SlicedStatistics = {
/** Statistics of the part. */
global: GlobalStatistics;
/** Indices of its first and last trackpoints in the statistics of the selection. */
start: number;
end: number;
};
export const slicedStatistics = writable<SlicedStatistics | undefined>(undefined);
// the indices only make sense for the selection they were made on
engine.statistics.subscribe(() => slicedStatistics.set(undefined));
/** The point of the elevation profile that the pointer is over, shown on the map. */
export const hoveredPoint: Writable<Coordinates | null> = writable(null);
+220
View File
@@ -0,0 +1,220 @@
import assert from 'node:assert/strict';
import { afterEach, beforeEach, describe, it } from 'node:test';
import { get } from 'svelte/store';
import type { LayerTreeType } from '$lib/data/layers';
import {
filterLayerTree,
getArrayValidator,
getLayerTreeValidator,
getLayerValidator,
getValueValidator,
Setting,
SettingInitOnFirstRead,
storedValue,
useSettingsStorage,
} from '$lib/state/setting';
/** What the settings were told to keep. */
let saved: Record<string, string | undefined>;
beforeEach(() => {
saved = {};
useSettingsStorage({
save: (key, json) => {
saved[key] = json;
},
remove: (key) => {
saved[key] = undefined;
},
});
});
afterEach(() => useSettingsStorage(undefined));
describe('reading saved values', () => {
it('reads JSON, and nothing when it cannot', () => {
assert.deepEqual(storedValue({ a: '3' }, 'a'), { value: 3 });
assert.deepEqual(storedValue({ a: '"text"' }, 'a'), { value: 'text' });
assert.deepEqual(storedValue({ a: 'null' }, 'a'), { value: null });
assert.equal(storedValue({ a: '{broken' }, 'a'), undefined);
assert.equal(storedValue({}, 'a'), undefined);
// a key of Object.prototype is not a saved key
assert.equal(storedValue({}, 'toString'), undefined);
});
});
describe('validators', () => {
it('keeps an allowed value, else the fallback', () => {
const validate = getValueValidator(['metric', 'imperial'], 'metric');
assert.equal(validate('imperial'), 'imperial');
assert.equal(validate('nautical'), 'metric');
assert.equal(validate(undefined as any), 'metric');
const optional = getValueValidator<string | undefined>(['slope', undefined], undefined);
assert.equal(optional('slope'), 'slope');
assert.equal(optional('other'), undefined);
});
it('keeps the allowed values of a list', () => {
const validate = getArrayValidator(['hr', 'cad', 'power']);
assert.deepEqual(validate(['power', 'speed', 'hr', 'hr']), ['power', 'hr', 'hr']);
assert.deepEqual(validate([]), []);
});
it('accepts the known layers, and the custom and extension ones', () => {
const validate = getLayerValidator({ libertyTopo: 'url', osm: 'url' }, 'libertyTopo');
assert.equal(validate('osm'), 'osm');
assert.equal(validate('custom-1234'), 'custom-1234');
assert.equal(validate('extension-some-id'), 'extension-some-id');
assert.equal(validate('gone'), 'libertyTopo');
assert.equal(validate('toString'), 'libertyTopo');
});
});
describe('layer trees', () => {
const allowed: LayerTreeType = {
world: { a: true, b: false },
countries: { france: { c: false } },
};
it('keeps the choices made on the layers that are allowed', () => {
const filtered = filterLayerTree({ world: { a: false, b: true } }, allowed);
assert.deepEqual(filtered.world as LayerTreeType, { a: false, b: true });
});
it('adds the layers that are new, with their default', () => {
const filtered = filterLayerTree({ world: { a: false } }, allowed);
assert.deepEqual(filtered, {
world: { a: false, b: false },
countries: { france: { c: false } },
});
assert.deepEqual(filterLayerTree({}, allowed), allowed);
});
it('drops the layers that are not allowed anymore, but not the custom ones', () => {
const filtered = filterLayerTree(
{
world: { a: true, gone: true, 'custom-1': true, 'extension-x': false },
old: { x: true },
},
allowed
);
assert.deepEqual(filtered.world, {
a: true,
b: false,
'custom-1': true,
'extension-x': false,
});
assert.equal('old' in filtered, true);
assert.deepEqual(filtered.old, {});
});
it('does not take a branch for a layer, or a layer for a branch', () => {
const filtered = filterLayerTree({ world: true, countries: { france: false } }, allowed);
assert.deepEqual(filtered.world, { a: true, b: false });
assert.deepEqual((filtered.countries as LayerTreeType).france, { c: false });
assert.deepEqual(getLayerTreeValidator(allowed)({ world: { a: false } }).world, {
a: false,
b: false,
});
});
});
describe('Setting', () => {
it('has its initial value until it is connected', () => {
const setting = new Setting('key', 'initial');
assert.equal(get(setting), 'initial');
setting.connect({});
assert.equal(get(setting), 'initial');
setting.connect({ other: '"x"' });
assert.equal(get(setting), 'initial');
});
it('takes the saved value, validated', () => {
const setting = new Setting(
'key',
'metric',
getValueValidator(['metric', 'imperial'], 'metric')
);
setting.connect({ key: '"imperial"' });
assert.equal(get(setting), 'imperial');
setting.connect({ key: '"furlongs"' });
assert.equal(get(setting), 'metric');
setting.connect({ key: '{broken' });
assert.equal(get(setting), 'metric');
// connecting does not save anything
assert.deepEqual(saved, {});
});
it('saves what it is set to, when it changes', () => {
const setting = new Setting('key', 1);
setting.set(2);
assert.equal(get(setting), 2);
assert.deepEqual(saved, { key: '2' });
saved = {};
setting.set(2);
assert.deepEqual(saved, {});
setting.update((value) => value + 1);
assert.equal(get(setting), 3);
assert.deepEqual(saved, { key: '3' });
});
it('always saves objects, which may have been changed in place', () => {
const value = { a: 1 };
const setting = new Setting('key', value);
setting.set(value);
assert.deepEqual(saved, { key: '{"a":1}' });
});
it('forgets what it saved when it has no value', () => {
const setting = new Setting<string | undefined>('key', 'x');
setting.set(undefined);
assert.equal(get(setting), undefined);
assert.ok('key' in saved);
assert.equal(saved.key, undefined);
});
it('tells its subscribers', () => {
const setting = new Setting('key', 1);
const seen: number[] = [];
const unsubscribe = setting.subscribe((value) => seen.push(value));
setting.set(2);
setting.set(2);
unsubscribe();
setting.set(3);
assert.deepEqual(seen, [1, 2]);
});
});
describe('SettingInitOnFirstRead', () => {
it('has no value until it is connected', () => {
const setting = new SettingInitOnFirstRead('key', { a: true });
assert.equal(get(setting), undefined);
setting.connect({});
assert.deepEqual(get(setting), { a: true });
});
it('takes the saved value, validated, instead of the initial one', () => {
const setting = new SettingInitOnFirstRead('key', ['hr'], getArrayValidator(['hr', 'cad']));
setting.connect({ key: '["cad","nope"]' });
assert.deepEqual(get(setting), ['cad']);
// nothing is saved when it is connected, that is what `initialize` is for
assert.deepEqual(saved, {});
});
it('can be initialized, which saves the initial value', () => {
const setting = new SettingInitOnFirstRead('key', 5);
setting.initialize();
assert.equal(get(setting), 5);
assert.deepEqual(saved, { key: '5' });
});
});
describe('Setting without storage', () => {
it('changes, but keeps nothing', () => {
useSettingsStorage(undefined);
const setting = new Setting('key', 1);
setting.set(2);
assert.equal(get(setting), 2);
assert.deepEqual(saved, {});
});
});
+173
View File
@@ -0,0 +1,173 @@
import { get, writable, type Writable } from 'svelte/store';
import type { LayerTreeType } from '$lib/data/layers';
/** What the settings were saved as, by key: JSON strings (see `Engine.openStorage`). */
export type StoredSettings = Record<string, string>;
/** Where the settings are kept. */
export type SettingsStorage = {
/** Keeps the JSON of a setting. */
save(key: string, json: string): void;
/** Forgets a setting. */
remove(key: string): void;
};
let storage: SettingsStorage | undefined;
/** Sets where the settings are kept: until then, changes are not saved. */
export function useSettingsStorage(newStorage: SettingsStorage | undefined) {
storage = newStorage;
}
/** Reads the saved value of a setting, `undefined` if there is none or if it cannot be read. */
export function storedValue(stored: StoredSettings, key: string): { value: any } | undefined {
if (!Object.hasOwn(stored, key)) {
return undefined;
}
try {
return { value: JSON.parse(stored[key]) };
} catch {
return undefined;
}
}
/** Keeps a value; `undefined` is not something JSON can hold, it means no value. */
function save(key: string, value: unknown) {
const json = JSON.stringify(value);
if (json === undefined) {
storage?.remove(key);
} else {
storage?.save(key, json);
}
}
/**
* A setting: a store whose value is kept when it changes. `S` is what the store holds, which is
* `V` or, for a setting that has no value until it is read, `V | undefined`.
*/
abstract class SettingBase<V, S extends V | undefined> {
protected _key: string;
protected _value: Writable<S>;
protected _validator?: (value: V) => V;
protected constructor(key: string, initial: S, validator?: (value: V) => V) {
this._key = key;
this._value = writable(initial);
this._validator = validator;
}
/** The saved value, validated, `undefined` if there is none. */
protected saved(stored: StoredSettings): { value: V } | undefined {
const saved = storedValue(stored, this._key);
return saved && { value: this._validator ? this._validator(saved.value) : saved.value };
}
/** Takes the saved value, if there is one. */
abstract connect(stored: StoredSettings): void;
subscribe(run: (value: S) => void, invalidate?: (value?: S) => void) {
return this._value.subscribe(run, invalidate);
}
/** Sets the value and keeps it. An object is always kept: it may have been changed in place. */
set(value: V) {
if (typeof value === 'object' || value !== get(this._value)) {
this._value.set(value as S);
save(this._key, value);
}
}
update(callback: (value: any) => any) {
this.set(callback(get(this._value)));
}
}
export class Setting<V> extends SettingBase<V, V> {
constructor(key: string, initial: V, validator?: (value: V) => V) {
super(key, initial, validator);
}
connect(stored: StoredSettings) {
const saved = this.saved(stored);
if (saved) {
this._value.set(saved.value);
}
}
}
/** A setting that has no value until it is connected: then the saved one, or the initial one. */
export class SettingInitOnFirstRead<V> extends SettingBase<V, V | undefined> {
private _initial: V;
constructor(key: string, initial: V, validator?: (value: V) => V) {
super(key, undefined, validator);
this._initial = initial;
}
connect(stored: StoredSettings) {
this._value.set(this.saved(stored)?.value ?? this._initial);
}
/** Takes the initial value and keeps it, for a setting that was never saved. */
initialize() {
this.set(this._initial);
}
}
export function getValueValidator<V>(allowed: V[], fallback: V) {
const dict = new Set<V>(allowed);
return (value: V) => (dict.has(value) ? value : fallback);
}
export function getArrayValidator<V>(allowed: V[]) {
const dict = new Set<V>(allowed);
return (value: V[]) => value.filter((v) => dict.has(v));
}
export function getLayerValidator(allowed: Record<string, any>, fallback: string) {
return (layer: string) =>
allowed.hasOwnProperty(layer) ||
layer.startsWith('custom-') ||
layer.startsWith('extension-')
? layer
: fallback;
}
export function filterLayerTree(
t: LayerTreeType,
allowed: LayerTreeType | undefined
): LayerTreeType {
const filtered: LayerTreeType = {};
if (allowed) {
Object.entries(allowed).forEach(([key, value]) => {
if (Object.hasOwn(t, key)) {
if (typeof value === 'boolean') {
filtered[key] = t[key];
} else if (typeof value === 'object') {
filtered[key] = filterLayerTree(
typeof t[key] === 'object' ? t[key] : {},
value
);
}
} else {
filtered[key] = value;
}
});
}
Object.entries(t).forEach(([key, value]) => {
if (!Object.hasOwn(filtered, key)) {
if (typeof value === 'boolean') {
if (key.startsWith('custom-') || key.startsWith('extension-')) {
filtered[key] = value;
}
} else if (typeof value === 'object') {
filtered[key] = filterLayerTree(value, undefined);
}
}
});
return filtered;
}
export function getLayerTreeValidator(allowed: LayerTreeType) {
return (value: LayerTreeType) => filterLayerTree(value, allowed);
}
+75
View File
@@ -0,0 +1,75 @@
import assert from 'node:assert/strict';
import { afterEach, beforeEach, describe, it } from 'node:test';
import { get } from 'svelte/store';
import { engine } from '$lib/engine';
import { settings } from '$lib/state/settings';
/** What the engine was told to keep. */
let saved: Record<string, string | undefined>;
/** What was kept, seen without what the assertions made of its type. */
const kept = (): Record<string, string | undefined> => saved;
const original = { set: engine.setSetting, delete: engine.deleteSetting };
beforeEach(() => {
saved = {};
engine.setSetting = (key, json) => {
saved[key] = json;
};
engine.deleteSetting = (key) => {
saved[key] = undefined;
};
});
afterEach(() => {
engine.setSetting = original.set;
engine.deleteSetting = original.delete;
});
describe('settings', () => {
it('connects every setting', () => {
settings.connect({
distanceUnits: '"imperial"',
velocityUnits: '"pace"',
temperatureUnits: '"fahrenheit"',
routingProfile: '"foot"',
currentOverlays: '{"cyclOSM":true}',
additionalDatasets: '["hr","bogus"]',
});
assert.equal(get(settings.distanceUnits), 'imperial');
assert.equal(get(settings.velocityUnits), 'pace');
assert.equal(get(settings.temperatureUnits), 'fahrenheit');
assert.equal(get(settings.routingProfile), 'foot');
assert.deepEqual(get(settings.additionalDatasets), ['hr']);
assert.ok(get(settings.currentOverlays));
// values that are not valid anymore are replaced
settings.connect({ distanceUnits: '"cubits"', routingProfile: '"hovercraft"' });
assert.equal(get(settings.distanceUnits), 'metric');
assert.equal(get(settings.routingProfile), 'bike');
});
it('initializes the settings that have no value until they are read', () => {
settings.connect({});
saved = {};
settings.initialize();
assert.ok('currentOverlays' in saved);
assert.ok('currentOverpassQueries' in saved);
assert.equal(Object.keys(saved).length, 2);
});
it('are kept by the engine when they change', () => {
settings.connect({});
settings.distanceUnits.set('nautical');
assert.deepEqual(saved, { distanceUnits: '"nautical"' });
// a setting that has no value is forgotten
settings.elevationFill.set('slope');
assert.equal(kept().elevationFill, '"slope"');
settings.elevationFill.set(undefined);
assert.ok('elevationFill' in kept() && kept().elevationFill === undefined);
saved = {};
// the same value is not kept again
settings.distanceUnits.set('nautical');
assert.deepEqual(saved, {});
});
});
+176
View File
@@ -0,0 +1,176 @@
import {
basemaps,
defaultBasemap,
defaultBasemapTree,
defaultOpacities,
defaultOverlays,
defaultOverlayTree,
defaultOverpassQueries,
defaultOverpassTree,
defaultTerrainSource,
overpassTree,
type CustomLayer,
type LayerTreeType,
} from '$lib/data/layers';
import { browser } from '$app/environment';
import type { DistanceUnits, TemperatureUnits, VelocityUnits } from '$lib/units/conversions';
import { engine } from '$lib/engine';
import {
getArrayValidator,
getLayerTreeValidator,
getLayerValidator,
getValueValidator,
Setting,
SettingInitOnFirstRead,
useSettingsStorage,
type StoredSettings,
} from '$lib/state/setting';
export type { StoredSettings };
type AdditionalDataset = 'speed' | 'hr' | 'cad' | 'atemp' | 'power';
type ElevationFill = 'slope' | 'surface' | 'highway' | undefined;
type RoutingProfile =
| 'bike'
| 'racing_bike'
| 'gravel_bike'
| 'mountain_bike'
| 'foot'
| 'motorcycle'
| 'water'
| 'railway';
type TerrainSource = 'mapterhorn';
type StreetViewSource = 'mapillary' | 'google';
// The settings are kept by the engine, in the storage of the browser.
useSettingsStorage({
save: (key, json) => engine.setSetting(key, json),
remove: (key) => engine.deleteSetting(key),
});
export const settings = {
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')
),
elevationProfile: new Setting<boolean>('elevationProfile', true),
additionalDatasets: new Setting<AdditionalDataset[]>(
'additionalDatasets',
[],
getArrayValidator<AdditionalDataset>(['speed', 'hr', 'cad', 'atemp', 'power'])
),
elevationFill: new Setting<ElevationFill>(
'elevationFill',
undefined,
getValueValidator(['slope', 'surface', 'highway', undefined], undefined)
),
treeFileView: new Setting<boolean>('fileView', false),
minimizeRoutingMenu: new Setting('minimizeRoutingMenu', false),
routing: new Setting('routing', true),
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),
currentBasemap: new Setting(
'currentBasemap',
defaultBasemap,
getLayerValidator(basemaps, defaultBasemap)
),
previousBasemap: new Setting(
'previousBasemap',
defaultBasemap,
getLayerValidator(basemaps, defaultBasemap)
),
selectedBasemapTree: new Setting(
'selectedBasemapTree',
defaultBasemapTree,
getLayerTreeValidator(defaultBasemapTree)
),
currentOverlays: new SettingInitOnFirstRead(
'currentOverlays',
defaultOverlays,
getLayerTreeValidator(defaultOverlayTree)
),
previousOverlays: new Setting(
'previousOverlays',
defaultOverlays,
getLayerTreeValidator(defaultOverlayTree)
),
selectedOverlayTree: new Setting(
'selectedOverlayTree',
defaultOverlayTree,
getLayerTreeValidator(defaultOverlayTree)
),
currentOverpassQueries: new SettingInitOnFirstRead(
'currentOverpassQueries',
defaultOverpassQueries,
getLayerTreeValidator(overpassTree)
),
selectedOverpassTree: new Setting(
'selectedOverpassTree',
defaultOverpassTree,
getLayerTreeValidator(overpassTree)
),
opacities: new Setting('opacities', defaultOpacities),
customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}),
customBasemapOrder: new Setting<string[]>('customBasemapOrder', []),
customOverlayOrder: new Setting<string[]>('customOverlayOrder', []),
terrainSource: new Setting<TerrainSource>(
'terrainSource',
defaultTerrainSource,
getValueValidator(['mapterhorn'], defaultTerrainSource)
),
directionMarkers: new Setting('directionMarkers', false),
distanceMarkers: new Setting('distanceMarkers', false),
streetViewSource: new Setting<StreetViewSource>(
'streetViewSource',
'mapillary',
getValueValidator<StreetViewSource>(['mapillary', 'google'], 'mapillary')
),
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),
/** Takes the saved values (see `Engine.openStorage`). */
connect(stored: StoredSettings) {
for (const key in settings) {
const setting = (settings as any)[key];
if (setting instanceof Setting || setting instanceof SettingInitOnFirstRead) {
setting.connect(stored);
}
}
},
initialize() {
for (const key in settings) {
const setting = (settings as any)[key];
if (setting instanceof SettingInitOnFirstRead) {
setting.initialize();
}
}
},
};