fix embedding + playground

This commit is contained in:
vcoppe
2025-11-09 18:03:27 +01:00
parent ec3eb387e5
commit 59710d2e1a
14 changed files with 307 additions and 422 deletions

View File

@@ -2,61 +2,12 @@ import { get } from 'svelte/store';
import { selection } from '$lib/logic/selection';
import mapboxgl from 'mapbox-gl';
import { ListFileItem, ListWaypointItem } from '$lib/components/file-list/file-list';
import {
fileStateCollection,
GPXFileState,
GPXFileStateCollectionObserver,
} from '$lib/logic/file-state';
import { fileStateCollection, GPXFileStateCollectionObserver } from '$lib/logic/file-state';
import { gpxStatistics } from '$lib/logic/statistics';
import { map } from '$lib/components/map/map';
import type { GPXFileWithStatistics } from './statistics-tree';
import type { Coordinates } from 'gpx';
import { page } from '$app/state';
import { browser } from '$app/environment';
// const targetMapBounds: {
// bounds: mapboxgl.LngLatBounds;
// ids: string[];
// total: number;
// } = $state({
// bounds: new mapboxgl.LngLatBounds([180, 90, -180, -90]),
// ids: [],
// total: 0,
// });
// $effect(() => {
// if (
// map.current === null ||
// targetMapBounds.ids.length > 0 ||
// (targetMapBounds.bounds.getSouth() === 90 &&
// targetMapBounds.bounds.getWest() === 180 &&
// targetMapBounds.bounds.getNorth() === -90 &&
// targetMapBounds.bounds.getEast() === -180)
// ) {
// return;
// }
// let currentZoom = map.current.getZoom();
// let currentBounds = map.current.getBounds();
// if (
// targetMapBounds.total !== get(fileObservers).size &&
// currentBounds &&
// currentZoom > 2 // Extend current bounds only if the map is zoomed in
// ) {
// // There are other files on the map
// if (
// currentBounds.contains(targetMapBounds.bounds.getSouthEast()) &&
// currentBounds.contains(targetMapBounds.bounds.getNorthWest())
// ) {
// return;
// }
// targetMapBounds.bounds.extend(currentBounds.getSouthWest());
// targetMapBounds.bounds.extend(currentBounds.getNorthEast());
// }
// map.current.fitBounds(targetMapBounds.bounds, { padding: 80, linear: true, easing: () => 1 });
// });
export class BoundsManager {
private _bounds: mapboxgl.LngLatBounds = new mapboxgl.LngLatBounds();

View File

@@ -2,11 +2,7 @@ import { db, type Database } from '$lib/db';
import { liveQuery } from 'dexie';
import type { GPXFile } from 'gpx';
import { applyPatches, produceWithPatches, type Patch, type WritableDraft } from 'immer';
import {
fileStateCollection,
GPXFileStateCollectionObserver,
type GPXFileStateCollection,
} from '$lib/logic/file-state';
import { GPXFileStateCollectionObserver } from '$lib/logic/file-state';
import {
derived,
get,
@@ -30,7 +26,7 @@ export class FileActionManager {
private _canUndo: Readable<boolean>;
private _canRedo: Readable<boolean>;
constructor(db: Database, fileStateCollection: GPXFileStateCollection) {
constructor(db: Database) {
this._db = db;
this._files = new Map();
this._fileSubscriptions = new Map();
@@ -156,7 +152,7 @@ export class FileActionManager {
selection.updateFiles(updatedFiles, deletedFileIds);
// @ts-ignore
return db.transaction('rw', db.fileids, db.files, async () => {
return this._db.transaction('rw', this._db.fileids, this._db.files, async () => {
if (updatedFileIds.length > 0) {
await this._db.fileids.bulkPut(updatedFileIds, updatedFileIds);
await this._db.files.bulkPut(updatedFiles, updatedFileIds);
@@ -254,4 +250,4 @@ function getChangedFileIds(patch: Patch[]): string[] {
return Array.from(changedFileIds);
}
export const fileActionManager = new FileActionManager(db, fileStateCollection);
export const fileActionManager = new FileActionManager(db);

View File

@@ -1,5 +1,5 @@
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/simplify';
import { db, type Database } from '$lib/db';
import { type Database } from '$lib/db';
import { liveQuery } from 'dexie';
import { GPXFile } from 'gpx';
import { GPXStatisticsTree, type GPXFileWithStatistics } from '$lib/logic/statistics-tree';
@@ -8,13 +8,18 @@ import { get, writable, type Subscriber, type Writable } from 'svelte/store';
// Observe a single file from the database, and maintain its statistics
export class GPXFileState {
private _fileId: string;
private _file: Writable<GPXFileWithStatistics | undefined>;
private _subscription: { unsubscribe: () => void } | undefined;
constructor(db: Database, fileId: string) {
this._file = writable(undefined);
constructor(fileId: string, file?: GPXFile) {
this._fileId = fileId;
this._file = writable(file ? { file, statistics: new GPXStatisticsTree(file) } : undefined);
}
this._subscription = liveQuery(() => db.files.get(fileId)).subscribe((value) => {
connectToDatabase(db: Database) {
if (this._subscription) return;
this._subscription = liveQuery(() => db.files.get(this._fileId)).subscribe((value) => {
if (value !== undefined) {
let file = new GPXFile(value);
updateAnchorPoints(file);
@@ -45,11 +50,15 @@ export class GPXFileState {
// Observe the file ids in the database, and maintain a map of file states for the corresponding files
export class GPXFileStateCollection {
private _files: Writable<Map<string, GPXFileState>>;
private _subscription: { unsubscribe: () => void } | null = null;
constructor(db: Database) {
constructor() {
this._files = writable(new Map());
}
liveQuery(() => db.fileids.toArray()).subscribe((dbFileIds) => {
connectToDatabase(db: Database) {
if (this._subscription) return;
this._subscription = liveQuery(() => db.fileids.toArray()).subscribe((dbFileIds) => {
const currentFiles = get(this._files);
// Find new files to observe
let newFiles = dbFileIds
@@ -64,7 +73,9 @@ export class GPXFileStateCollection {
// Update the map of file states
this._files.update(($files) => {
newFiles.forEach((id) => {
$files.set(id, new GPXFileState(db, id));
const fileState = new GPXFileState(id);
fileState.connectToDatabase(db);
$files.set(id, fileState);
});
deletedFiles.forEach((id) => {
$files.get(id)?.destroy();
@@ -85,6 +96,31 @@ export class GPXFileStateCollection {
});
}
disconnectFromDatabase() {
this._subscription?.unsubscribe();
this._subscription = null;
this._files.update(($files) => {
$files.forEach((fileState) => {
fileState.destroy();
});
return new Map();
});
}
setEmbeddedFiles(files: GPXFile[]) {
this._files.update(($files) => {
$files.clear();
files.forEach((file) => {
const id = file._data.id;
if (!$files.has(id)) {
const fileState = new GPXFileState(id, file);
$files.set(id, fileState);
}
});
return $files;
});
}
subscribe(run: Subscriber<Map<string, GPXFileState>>, invalidate?: () => void) {
return this._files.subscribe(run, invalidate);
}
@@ -117,7 +153,7 @@ export class GPXFileStateCollection {
}
// Collection of all file states
export const fileStateCollection = new GPXFileStateCollection(db);
export const fileStateCollection = new GPXFileStateCollection();
export type GPXFileStateCallback = (files: Map<string, GPXFileState>) => void;
export class GPXFileStateCollectionObserver {

View File

@@ -1,4 +1,4 @@
import { db, type Database } from '$lib/db';
import { type Database } from '$lib/db';
import { liveQuery } from 'dexie';
import {
defaultBasemap,
@@ -14,17 +14,22 @@ import { browser } from '$app/environment';
import { get, writable, type Writable } from 'svelte/store';
export class Setting<V> {
private _db: Database;
private _db: Database | null = null;
private _subscription: { unsubscribe: () => void } | null = null;
private _key: string;
private _value: Writable<V>;
constructor(db: Database, key: string, initial: V) {
this._db = db;
constructor(key: string, initial: V) {
this._key = key;
this._value = writable(initial);
}
connectToDatabase(db: Database) {
if (this._db) return;
this._db = db;
let first = true;
liveQuery(() => db.settings.get(key)).subscribe((value) => {
this._subscription = liveQuery(() => db.settings.get(this._key)).subscribe((value) => {
if (value === undefined) {
if (!first) {
this._value.set(value);
@@ -36,39 +41,53 @@ export class Setting<V> {
});
}
disconnectFromDatabase() {
this._subscription?.unsubscribe();
this._subscription = null;
this._db = null;
}
subscribe(run: (value: V) => void, invalidate?: (value?: V) => void) {
return this._value.subscribe(run, invalidate);
}
set(newValue: V) {
if (typeof newValue === 'object' || newValue !== get(this._value)) {
this._db.settings.put(newValue, this._key);
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);
}
}
}
update(callback: (value: any) => any) {
let newValue = callback(get(this._value));
if (typeof newValue === 'object' || newValue !== get(this._value)) {
this._db.settings.put(newValue, this._key);
}
this.set(callback(get(this._value)));
}
}
export class SettingInitOnFirstRead<V> {
private _db: Database;
private _db: Database | null = null;
private _subscription: { unsubscribe: () => void } | null = null;
private _key: string;
private _value: Writable<V | undefined>;
private _initial: V;
constructor(db: Database, key: string, initial: V) {
this._db = db;
constructor(key: string, initial: V) {
this._key = key;
this._value = writable(undefined);
this._initial = initial;
}
connectToDatabase(db: Database) {
if (this._db) return;
this._db = db;
let first = true;
liveQuery(() => db.settings.get(key)).subscribe((value) => {
this._subscription = liveQuery(() => db.settings.get(this._key)).subscribe((value) => {
if (value === undefined) {
if (first) {
this._value.set(initial);
this._value.set(this._initial);
} else {
this._value.set(value);
}
@@ -79,58 +98,80 @@ export class SettingInitOnFirstRead<V> {
});
}
disconnectFromDatabase() {
this._subscription?.unsubscribe();
this._subscription = null;
this._db = null;
}
subscribe(run: (value: V | undefined) => void, invalidate?: (value?: V | undefined) => void) {
return this._value.subscribe(run, invalidate);
}
set(newValue: V) {
if (typeof newValue === 'object' || newValue !== get(this._value)) {
this._db.settings.put(newValue, this._key);
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);
}
}
}
update(callback: (value: any) => any) {
let newValue = callback(get(this._value));
if (typeof newValue === 'object' || newValue !== get(this._value)) {
this._db.settings.put(newValue, this._key);
}
this.set(callback(get(this._value)));
}
}
export const settings = {
distanceUnits: new Setting<'metric' | 'imperial' | 'nautical'>(db, 'distanceUnits', 'metric'),
velocityUnits: new Setting<'speed' | 'pace'>(db, 'velocityUnits', 'speed'),
temperatureUnits: new Setting<'celsius' | 'fahrenheit'>(db, 'temperatureUnits', 'celsius'),
elevationProfile: new Setting<boolean>(db, 'elevationProfile', true),
additionalDatasets: new Setting<string[]>(db, 'additionalDatasets', []),
elevationFill: new Setting<'slope' | 'surface' | undefined>(db, 'elevationFill', undefined),
treeFileView: new Setting<boolean>(db, 'fileView', false),
minimizeRoutingMenu: new Setting(db, 'minimizeRoutingMenu', false),
routing: new Setting(db, 'routing', true),
routingProfile: new Setting(db, 'routingProfile', 'bike'),
privateRoads: new Setting(db, 'privateRoads', false),
currentBasemap: new Setting(db, 'currentBasemap', defaultBasemap),
previousBasemap: new Setting(db, 'previousBasemap', defaultBasemap),
selectedBasemapTree: new Setting(db, 'selectedBasemapTree', defaultBasemapTree),
currentOverlays: new SettingInitOnFirstRead(db, 'currentOverlays', defaultOverlays),
previousOverlays: new Setting(db, 'previousOverlays', defaultOverlays),
selectedOverlayTree: new Setting(db, 'selectedOverlayTree', defaultOverlayTree),
distanceUnits: new Setting<'metric' | 'imperial' | 'nautical'>('distanceUnits', 'metric'),
velocityUnits: new Setting<'speed' | 'pace'>('velocityUnits', 'speed'),
temperatureUnits: new Setting<'celsius' | 'fahrenheit'>('temperatureUnits', 'celsius'),
elevationProfile: new Setting<boolean>('elevationProfile', true),
additionalDatasets: new Setting<string[]>('additionalDatasets', []),
elevationFill: new Setting<'slope' | 'surface' | undefined>('elevationFill', undefined),
treeFileView: new Setting<boolean>('fileView', false),
minimizeRoutingMenu: new Setting('minimizeRoutingMenu', false),
routing: new Setting('routing', true),
routingProfile: new Setting('routingProfile', '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),
currentOverpassQueries: new SettingInitOnFirstRead(
db,
'currentOverpassQueries',
defaultOverpassQueries
),
selectedOverpassTree: new Setting(db, 'selectedOverpassTree', defaultOverpassTree),
opacities: new Setting(db, 'opacities', defaultOpacities),
customLayers: new Setting<Record<string, CustomLayer>>(db, 'customLayers', {}),
customBasemapOrder: new Setting<string[]>(db, 'customBasemapOrder', []),
customOverlayOrder: new Setting<string[]>(db, 'customOverlayOrder', []),
directionMarkers: new Setting(db, 'directionMarkers', false),
distanceMarkers: new Setting(db, 'distanceMarkers', false),
streetViewSource: new Setting(db, 'streetViewSource', 'mapillary'),
fileOrder: new Setting<string[]>(db, 'fileOrder', []),
defaultOpacity: new Setting(db, 'defaultOpacity', 0.7),
defaultWidth: new Setting(db, 'defaultWidth', browser && window.innerWidth < 600 ? 8 : 5),
bottomPanelSize: new Setting(db, 'bottomPanelSize', 170),
rightPanelSize: new Setting(db, 'rightPanelSize', 240),
selectedOverpassTree: new Setting('selectedOverpassTree', defaultOverpassTree),
opacities: new Setting('opacities', defaultOpacities),
customLayers: new Setting<Record<string, CustomLayer>>('customLayers', {}),
customBasemapOrder: new Setting<string[]>('customBasemapOrder', []),
customOverlayOrder: new Setting<string[]>('customOverlayOrder', []),
directionMarkers: new Setting('directionMarkers', false),
distanceMarkers: new Setting('distanceMarkers', false),
streetViewSource: new Setting('streetViewSource', 'mapillary'),
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();
}
}
},
};