From 6c9faf54b1046f4ecde349920b8805a7501fd77b Mon Sep 17 00:00:00 2001 From: vcoppe Date: Fri, 3 May 2024 15:59:34 +0200 Subject: [PATCH] dexie progress --- gpx/src/gpx.ts | 117 ++++++++++--- website/src/lib/components/FileList.svelte | 8 +- .../src/lib/components/FileListItem.svelte | 68 ++++---- website/src/lib/components/Menu.svelte | 4 +- .../src/lib/components/gpx-layer/GPXLayer.ts | 52 ++++-- .../lib/components/gpx-layer/GPXLayers.svelte | 2 +- .../toolbar/tools/routing/Routing.svelte | 2 +- .../toolbar/tools/routing/RoutingControls.ts | 31 ++-- website/src/lib/db.ts | 163 ++++++++++++------ website/src/lib/stores.ts | 62 ++++--- 10 files changed, 334 insertions(+), 175 deletions(-) diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index 31d82829..0263c012 100644 --- a/gpx/src/gpx.ts +++ b/gpx/src/gpx.ts @@ -163,12 +163,17 @@ export class GPXFile extends GPXTreeNode{ constructor(gpx?: GPXFileType | GPXFile) { super(); if (gpx) { - this.attributes = cloneJSON(gpx.attributes); - this.metadata = cloneJSON(gpx.metadata); - this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : []; - this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : []; - if (gpx instanceof GPXFile && gpx._data) { - this._data = cloneJSON(gpx._data); + this.attributes = gpx.attributes + this.metadata = gpx.metadata; + if (gpx instanceof GPXFile) { + this.wpt = gpx.wpt; + this.trk = gpx.trk; + } else { + this.wpt = gpx.wpt ? gpx.wpt.map((waypoint) => new Waypoint(waypoint)) : []; + this.trk = gpx.trk ? gpx.trk.map((track) => new Track(track)) : []; + } + if (gpx.hasOwnProperty('_data')) { + this._data = gpx._data; } } else { this.attributes = {}; @@ -183,7 +188,13 @@ export class GPXFile extends GPXTreeNode{ } clone(): GPXFile { - return new GPXFile(this); + return new GPXFile({ + attributes: cloneJSON(this.attributes), + metadata: cloneJSON(this.metadata), + wpt: this.wpt.map((waypoint) => waypoint.clone()), + trk: this.trk.map((track) => track.clone()), + _data: cloneJSON(this._data), + }); } toGeoJSON(): GeoJSON.FeatureCollection { @@ -221,11 +232,15 @@ export class Track extends GPXTreeNode { this.cmt = track.cmt; this.desc = track.desc; this.src = track.src; - this.link = cloneJSON(track.link); + this.link = track.link; this.type = track.type; - this.trkseg = track.trkseg ? track.trkseg.map((seg) => new TrackSegment(seg)) : []; + if (track instanceof Track) { + this.trkseg = track.trkseg; + } else { + this.trkseg = track.trkseg ? track.trkseg.map((seg) => new TrackSegment(seg)) : []; + } this.extensions = cloneJSON(track.extensions); - if (track instanceof Track && track._data) { + if (track.hasOwnProperty('_data')) { this._data = cloneJSON(track._data); } } else { @@ -237,6 +252,20 @@ export class Track extends GPXTreeNode { return this.trkseg; } + clone(): Track { + return new Track({ + name: this.name, + cmt: this.cmt, + desc: this.desc, + src: this.src, + link: cloneJSON(this.link), + type: this.type, + trkseg: this.trkseg.map((seg) => seg.clone()), + extensions: cloneJSON(this.extensions), + _data: cloneJSON(this._data), + }); + } + toGeoJSON(): GeoJSON.Feature[] { return this.getChildren().map((child) => { let geoJSON = child.toGeoJSON(); @@ -267,10 +296,6 @@ export class Track extends GPXTreeNode { extensions: this.extensions, }; } - - clone(): Track { - return new Track(this); - } }; // A class that represents a TrackSegment in a GPX file @@ -282,15 +307,27 @@ export class TrackSegment extends GPXTreeLeaf { constructor(segment?: TrackSegmentType | TrackSegment) { super(); if (segment) { - this.trkpt = segment.trkpt.map((point) => new TrackPoint(point)); - if (segment instanceof TrackSegment && segment._data) { + if (segment instanceof TrackSegment) { + this.trkpt = segment.trkpt; + this.statistics = segment.statistics; + this.trkptStatistics = segment.trkptStatistics; + } else { + this.trkpt = segment.trkpt.map((point) => new TrackPoint(point)); + if (segment.hasOwnProperty('statistics') && segment.hasOwnProperty('trkptStatistics')) { + this.statistics = segment.statistics; + this.trkptStatistics = segment.trkptStatistics; + } + } + if (segment.hasOwnProperty('_data')) { this._data = cloneJSON(segment._data); } } else { this.trkpt = []; } - this._computeStatistics(); + if (!this.statistics) { + this._computeStatistics(); + } } _computeStatistics(): void { @@ -468,7 +505,10 @@ export class TrackSegment extends GPXTreeLeaf { } clone(): TrackSegment { - return new TrackSegment(this); + return new TrackSegment({ + trkpt: this.trkpt.map((point) => point.clone()), + _data: cloneJSON(this._data), + }); } }; @@ -480,14 +520,13 @@ export class TrackPoint { _data: { [key: string]: any } = {}; constructor(point: TrackPointType | TrackPoint) { - this.attributes = cloneJSON(point.attributes); + this.attributes = point.attributes; + this.attributes = point.attributes; this.ele = point.ele; - if (point.time) { - this.time = new Date(point.time.getTime()); - } - this.extensions = cloneJSON(point.extensions); - if (point instanceof TrackPoint && point._data) { - this._data = cloneJSON(point._data); + this.time = point.time; + this.extensions = point.extensions; + if (point.hasOwnProperty('_data')) { + this._data = point._data; } } @@ -548,6 +587,16 @@ export class TrackPoint { extensions: this.extensions, }; } + + clone(): TrackPoint { + return new TrackPoint({ + attributes: cloneJSON(this.attributes), + ele: this.ele, + time: this.time ? new Date(this.time.getTime()) : undefined, + extensions: cloneJSON(this.extensions), + _data: cloneJSON(this._data), + }); + } }; export class Waypoint { @@ -562,7 +611,7 @@ export class Waypoint { type?: string; constructor(waypoint: WaypointType | Waypoint) { - this.attributes = cloneJSON(waypoint.attributes); + this.attributes = waypoint.attributes; this.ele = waypoint.ele; if (waypoint.time) { this.time = new Date(waypoint.time.getTime()); @@ -570,7 +619,7 @@ export class Waypoint { this.name = waypoint.name; this.cmt = waypoint.cmt; this.desc = waypoint.desc; - this.link = cloneJSON(waypoint.link); + this.link = waypoint.link; this.sym = waypoint.sym; this.type = waypoint.type; } @@ -582,6 +631,20 @@ export class Waypoint { setCoordinates(coordinates: Coordinates): void { this.attributes = coordinates; } + + clone(): Waypoint { + return new Waypoint({ + attributes: cloneJSON(this.attributes), + ele: this.ele, + time: this.time ? new Date(this.time.getTime()) : undefined, + name: this.name, + cmt: this.cmt, + desc: this.desc, + link: cloneJSON(this.link), + sym: this.sym, + type: this.type, + }); + } } export class GPXStatistics { diff --git a/website/src/lib/components/FileList.svelte b/website/src/lib/components/FileList.svelte index bb93a951..bcc3e1d2 100644 --- a/website/src/lib/components/FileList.svelte +++ b/website/src/lib/components/FileList.svelte @@ -123,14 +123,14 @@ }); -
+
{#if $fileObservers} - {#each $fileObservers.values() as file} + {#each $fileObservers.entries() as [fileId, file]}
diff --git a/website/src/lib/components/FileListItem.svelte b/website/src/lib/components/FileListItem.svelte index 1c6b1a21..cde4ecd2 100644 --- a/website/src/lib/components/FileListItem.svelte +++ b/website/src/lib/components/FileListItem.svelte @@ -4,43 +4,47 @@ import Shortcut from './Shortcut.svelte'; import { Copy, Trash2 } from 'lucide-svelte'; - import { get, type Readable, type Writable } from 'svelte/store'; + import { get, type Readable } from 'svelte/store'; import { selectedFiles, selectFiles } from '$lib/stores'; + import { dbUtils } from '$lib/db'; import { _ } from 'svelte-i18n'; import type { GPXFile } from 'gpx'; - import type { FreezedObject } from 'structurajs'; - import { dbUtils } from '$lib/db'; - export let file: Readable> | undefined; + export let file: Readable; -
{ - if (!get(selectedFiles).has($file?._data.id)) { - get(selectFiles).select($file?._data.id); - } - }} -> - - - - - - - - {$_('menu.duplicate')} - - - - {$_('menu.delete')} - - - -
+{#if $file} +
{ + if (!get(selectedFiles).has($file._data.id)) { + get(selectFiles).select($file._data.id); + } + }} + > + + + + + + + + {$_('menu.duplicate')} + + + + {$_('menu.delete')} + + + +
+{/if} diff --git a/website/src/lib/components/Menu.svelte b/website/src/lib/components/Menu.svelte index 3035d515..0ab11557 100644 --- a/website/src/lib/components/Menu.svelte +++ b/website/src/lib/components/Menu.svelte @@ -19,7 +19,7 @@ import { _ } from 'svelte-i18n'; import { derived, get } from 'svelte/store'; - import { canUndo, dbUtils, fileObservers, redo, undo } from '$lib/db'; + import { canUndo, canRedo, dbUtils, fileObservers, redo, undo } from '$lib/db'; let showDistanceMarkers = false; let showDirectionMarkers = false; @@ -41,7 +41,7 @@ } let undoDisabled = derived(canUndo, ($canUndo) => !$canUndo); - let redoDisabled = derived(canUndo, ($canUndo) => !$canUndo); + let redoDisabled = derived(canRedo, ($canRedo) => !$canRedo);
diff --git a/website/src/lib/components/gpx-layer/GPXLayer.ts b/website/src/lib/components/gpx-layer/GPXLayer.ts index a4b6cd82..f7df044f 100644 --- a/website/src/lib/components/gpx-layer/GPXLayer.ts +++ b/website/src/lib/components/gpx-layer/GPXLayer.ts @@ -39,31 +39,36 @@ function decrementColor(color: string) { export class GPXLayer { map: mapboxgl.Map; - file: Readable>; fileId: string; + file: Readable | undefined>; layerColor: string; popup: mapboxgl.Popup; popupElement: HTMLElement; markers: mapboxgl.Marker[] = []; unsubscribe: () => void; - addBinded: () => void = this.add.bind(this); + updateBinded: () => void = this.update.bind(this); selectOnClickBinded: (e: any) => void = this.selectOnClick.bind(this); - constructor(map: mapboxgl.Map, file: Readable>, popup: mapboxgl.Popup, popupElement: HTMLElement) { + constructor(map: mapboxgl.Map, fileId: string, file: Readable | undefined>, popup: mapboxgl.Popup, popupElement: HTMLElement) { this.map = map; - this.file = file; - this.fileId = get(file)._data.id; + this.fileId = fileId; + this.file = file this.layerColor = getColor(); this.popup = popup; this.popupElement = popupElement; - this.unsubscribe = file.subscribe(this.updateData.bind(this)); + this.unsubscribe = file.subscribe(this.update.bind(this)); - this.add(); - this.map.on('style.load', this.addBinded); + this.map.on('style.load', this.updateBinded); } - add() { + update() { + let file = get(this.file); + if (!file) { + return; + } + + let addedSource = false; if (!this.map.getSource(this.fileId)) { let data = this.getGeoJSON(); @@ -71,6 +76,7 @@ export class GPXLayer { type: 'geojson', data }); + addedSource = true; } if (!this.map.getLayer(this.fileId)) { @@ -93,16 +99,16 @@ export class GPXLayer { this.map.on('mouseenter', this.fileId, toPointerCursor); this.map.on('mouseleave', this.fileId, toDefaultCursor); } - } - updateData() { - let source = this.map.getSource(this.fileId); - if (source) { - source.setData(this.getGeoJSON()); + if (!addedSource) { + let source = this.map.getSource(this.fileId); + if (source) { + source.setData(this.getGeoJSON()); + } } let markerIndex = 0; - get(this.file).wpt.forEach((waypoint) => { // Update markers + file.wpt.forEach((waypoint) => { // Update markers if (markerIndex < this.markers.length) { this.markers[markerIndex].setLngLat(waypoint.getCoordinates()); } else { @@ -131,7 +137,7 @@ export class GPXLayer { this.map.off('click', this.fileId, this.selectOnClickBinded); this.map.off('mouseenter', this.fileId, toPointerCursor); this.map.off('mouseleave', this.fileId, toDefaultCursor); - this.map.off('style.load', this.addBinded); + this.map.off('style.load', this.updateBinded); this.map.removeLayer(this.fileId); this.map.removeSource(this.fileId); @@ -146,7 +152,9 @@ export class GPXLayer { } moveToFront() { - this.map.moveLayer(this.fileId); + if (this.map.getLayer(this.fileId)) { + this.map.moveLayer(this.fileId); + } } selectOnClick(e: any) { @@ -161,7 +169,15 @@ export class GPXLayer { } getGeoJSON(): GeoJSON.FeatureCollection { - let data = get(this.file).toGeoJSON(); + let file = get(this.file); + if (!file) { + return { + type: 'FeatureCollection', + features: [] + }; + } + + let data = file.toGeoJSON(); for (let feature of data.features) { if (!feature.properties) { feature.properties = {}; diff --git a/website/src/lib/components/gpx-layer/GPXLayers.svelte b/website/src/lib/components/gpx-layer/GPXLayers.svelte index 4c5caf83..69b49fd3 100644 --- a/website/src/lib/components/gpx-layer/GPXLayers.svelte +++ b/website/src/lib/components/gpx-layer/GPXLayers.svelte @@ -22,7 +22,7 @@ // add layers for new files $fileObservers.forEach((file, fileId) => { if (!$layers.has(fileId)) { - $layers.set(fileId, new GPXLayer(get(map), file, popup, popupElement)); + $layers.set(fileId, new GPXLayer(get(map), fileId, file, popup, popupElement)); } }); return $layers; diff --git a/website/src/lib/components/toolbar/tools/routing/Routing.svelte b/website/src/lib/components/toolbar/tools/routing/Routing.svelte index a16815c7..cf8011ac 100644 --- a/website/src/lib/components/toolbar/tools/routing/Routing.svelte +++ b/website/src/lib/components/toolbar/tools/routing/Routing.svelte @@ -61,7 +61,7 @@ if (selectedFileObserver) { routingControls.set( selectedId, - new RoutingControls(get(map), selectedFileObserver, popup, popupElement) + new RoutingControls(get(map), selectedId, selectedFileObserver, popup, popupElement) ); } } else { diff --git a/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts b/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts index 88173c30..ce764e4b 100644 --- a/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts +++ b/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts @@ -1,5 +1,5 @@ import { distance, type Coordinates, type GPXFile, TrackPoint, TrackSegment } from "gpx"; -import { get, type Writable } from "svelte/store"; +import { get, type Readable, type Writable } from "svelte/store"; import { computeAnchorPoints } from "./Simplify"; import mapboxgl from "mapbox-gl"; import { route } from "./Routing"; @@ -11,7 +11,8 @@ import { dbUtils } from "$lib/db"; export class RoutingControls { map: mapboxgl.Map; - file: Writable; + fileId: string = ''; + file: Readable; anchors: AnchorWithMarker[] = []; shownAnchors: AnchorWithMarker[] = []; popup: mapboxgl.Popup; @@ -24,8 +25,9 @@ export class RoutingControls { updateTemporaryAnchorBinded: (e: any) => void = this.updateTemporaryAnchor.bind(this); appendAnchorBinded: (e: mapboxgl.MapMouseEvent) => void = this.appendAnchor.bind(this); - constructor(map: mapboxgl.Map, file: Writable, popup: mapboxgl.Popup, popupElement: HTMLElement) { + constructor(map: mapboxgl.Map, fileId: string, file: Writable, popup: mapboxgl.Popup, popupElement: HTMLElement) { this.map = map; + this.fileId = fileId; this.file = file; this.popup = popup; this.popupElement = popupElement; @@ -46,13 +48,18 @@ export class RoutingControls { this.map.on('zoom', this.toggleAnchorsForZoomLevelAndBoundsBinded); this.map.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded); this.map.on('click', this.appendAnchorBinded); - this.map.on('mousemove', get(this.file)._data.id, this.showTemporaryAnchorBinded); + this.map.on('mousemove', this.fileId, this.showTemporaryAnchorBinded); this.unsubscribe = this.file.subscribe(this.updateControls.bind(this)); } updateControls() { // Update the markers when the file changes - let segments = get(this.file).getSegments(); + let file = get(this.file); + if (!file) { + return; + } + + let segments = file.getSegments(); let anchorIndex = 0; for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) { @@ -104,7 +111,7 @@ export class RoutingControls { this.map.off('zoom', this.toggleAnchorsForZoomLevelAndBoundsBinded); this.map.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded); this.map.off('click', this.appendAnchorBinded); - this.map.off('mousemove', get(this.file)._data.id, this.showTemporaryAnchorBinded); + this.map.off('mousemove', this.fileId, this.showTemporaryAnchorBinded); this.map.off('mousemove', this.updateTemporaryAnchorBinded); this.unsubscribe(); @@ -290,17 +297,17 @@ export class RoutingControls { let [previousAnchor, nextAnchor] = this.getNeighbouringAnchors(anchor); if (previousAnchor === null && nextAnchor === null) { // Only one point, remove it - dbUtils.applyToFile(get(this.file)._data.id, (file) => { + dbUtils.applyToFile(this.fileId, (file) => { let segment = file.getSegments()[anchor.segmentIndex]; segment.replace(0, 0, []); }); } else if (previousAnchor === null) { // First point, remove trackpoints until nextAnchor - dbUtils.applyToFile(get(this.file)._data.id, (file) => { + dbUtils.applyToFile(this.fileId, (file) => { let segment = file.getSegments()[anchor.segmentIndex]; segment.replace(0, nextAnchor.point._data.index - 1, []); }); } else if (nextAnchor === null) { // Last point, remove trackpoints from previousAnchor - dbUtils.applyToFile(get(this.file)._data.id, (file) => { + dbUtils.applyToFile(this.fileId, (file) => { let segment = file.getSegments()[anchor.segmentIndex]; segment.replace(previousAnchor.point._data.index + 1, segment.trkpt.length - 1, []); }); @@ -323,7 +330,7 @@ export class RoutingControls { if (!lastAnchor) { // TODO, create segment if it does not exist - dbUtils.applyToFile(get(this.file)._data.id, (file) => { + dbUtils.applyToFile(this.fileId, (file) => { let segment = file.getSegments()[0]; segment.replace(0, 0, [newPoint]); }); @@ -365,7 +372,7 @@ export class RoutingControls { let segment = anchors[0].segment; if (anchors.length === 1) { // Only one anchor, update the point in the segment - dbUtils.applyToFile(get(this.file)._data.id, (file) => { + dbUtils.applyToFile(this.fileId, (file) => { let segment = file.getSegments()[anchors[0].segmentIndex]; segment.replace(0, 0, [new TrackPoint({ attributes: targetCoordinates[0], @@ -425,7 +432,7 @@ export class RoutingControls { anchor.point._data.zoom = 0; // Make these anchors permanent }); - dbUtils.applyToFile(get(this.file)._data.id, (file) => { + dbUtils.applyToFile(this.fileId, (file) => { let segment = file.getSegments()[anchors[0].segmentIndex]; segment.replace(start, end, response); }); diff --git a/website/src/lib/db.ts b/website/src/lib/db.ts index 99a0c5e5..35365a30 100644 --- a/website/src/lib/db.ts +++ b/website/src/lib/db.ts @@ -6,49 +6,96 @@ import { fileOrder, selectedFiles } from './stores'; class Database extends Dexie { + fileids!: Dexie.Table; files!: Dexie.Table, string>; patches!: Dexie.Table<{ patch: Patch[], inversePatch: Patch[] }, number>; settings!: Dexie.Table; constructor() { - super("Database"); - this.version(1).stores({ - files: ',file', - patches: '++id,patch,inversePatch', - settings: ',value' + super("Database", { + cache: 'immutable' }); + this.version(1).stores({ + fileids: ',&fileid', + files: '', + patches: ',patch', + settings: '' + }); + this.files.add } } const db = new Database(); -function dexieStore(querier: () => T | Promise): Readable { - const dexieObservable = liveQuery(querier) - return { - subscribe(run, invalidate) { - return dexieObservable.subscribe(run, invalidate).unsubscribe +function dexieFileStore(querier: () => FreezedObject | undefined | Promise | undefined>): Readable { + let store = writable(undefined); + liveQuery(querier).subscribe(value => { + if (value !== undefined) { + let gpx = new GPXFile(value); + fileState.set(gpx._data.id, gpx); + store.set(gpx); } + }); + return { + subscribe: store.subscribe, + }; +} + +function dexieStore(querier: () => T | Promise, initial?: T): Readable { + let store = writable(initial); + liveQuery(querier).subscribe(value => { + if (value !== undefined) { + store.set(value); + } + }); + return { + subscribe: store.subscribe, + }; +} + +function updateFiles(files: (FreezedObject | undefined)[], add: boolean = false) { + let filteredFiles = files.filter(file => file !== undefined) as FreezedObject[]; + let fileIds = filteredFiles.map(file => file._data.id); + if (add) { + return db.transaction('rw', db.fileids, db.files, async () => { + await db.fileids.bulkAdd(fileIds, fileIds); + await db.files.bulkAdd(filteredFiles, fileIds); + }); + } else { + return db.files.bulkPut(filteredFiles, fileIds); } } -export function updateFiles(files: FreezedObject[]) { - console.log(files); - return db.files.bulkPut(files, files.map(file => file._data.id)); +function deleteFiles(fileIds: string[]) { + return db.transaction('rw', db.fileids, db.files, async () => { + await db.fileids.bulkDelete(fileIds); + await db.files.bulkDelete(fileIds); + }); } -export const fileObservers: Writable>>> = writable(new Map()); -export const fileState: Map> = new Map(); // Used to generate patches +function commitFileStateChange(newFileState: ReadonlyMap>, patch: Patch[]) { + if (newFileState.size > fileState.size) { + return updateFiles(getChangedFileIds(patch).map((fileId) => newFileState.get(fileId)), true); + } else if (newFileState.size === fileState.size) { + return updateFiles(getChangedFileIds(patch).map((fileId) => newFileState.get(fileId))); + } else { + return deleteFiles(getChangedFileIds(patch)); + } +} -liveQuery(() => db.files.toArray()).subscribe(dbFiles => { +export const fileObservers: Writable>> = writable(new Map()); +const fileState: Map = new Map(); // Used to generate patches + +liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => { // Find new files to observe - let newFiles = dbFiles.map(file => file._data.id).filter(id => !get(fileObservers).has(id)); + let newFiles = dbFileIds.filter(id => !get(fileObservers).has(id)); // Find deleted files to stop observing - let deletedFiles = Array.from(get(fileObservers).keys()).filter(id => !dbFiles.find(file => file._data.id === id)); + let deletedFiles = Array.from(get(fileObservers).keys()).filter(id => !dbFileIds.find(fileId => fileId === id)); // Update the store if (newFiles.length > 0 || deletedFiles.length > 0) { fileObservers.update($files => { newFiles.forEach(id => { - $files.set(id, dexieStore(() => db.files.get(id))); + $files.set(id, dexieFileStore(() => db.files.get(id))); }); deletedFiles.forEach(id => { $files.delete(id); @@ -56,71 +103,75 @@ liveQuery(() => db.files.toArray()).subscribe(dbFiles => { }); return $files; }); - console.log(get(fileObservers)); } - - // Update fileState - dbFiles.forEach(file => { - fileState.set(file._data.id, file); - }); }); -const patchIndex = dexieStore(() => db.settings.get('patchIndex') ?? -1); -const patches = dexieStore(() => db.patches.toArray()); -export const canUndo = derived(patchIndex, $patchIndex => $patchIndex >= 0); -export const canRedo = derived([patchIndex, patches], ([$patchIndex, $patches]) => $patchIndex < $patches.length - 1); +const patchIndex: Readable = dexieStore(() => db.settings.get('patchIndex'), -1); +const patches: Readable<{ patch: Patch[], inversePatch: Patch[] }[]> = dexieStore(() => db.patches.toArray(), []); +export const canUndo: Readable = derived(patchIndex, ($patchIndex) => $patchIndex >= 0); +export const canRedo: Readable = derived([patchIndex, patches], ([$patchIndex, $patches]) => $patchIndex < $patches.length - 1); export function applyGlobal(callback: (files: Map) => void) { const [newFileState, patch, inversePatch] = produceWithPatches(fileState, callback); appendPatches(patch, inversePatch, true); - return updateFiles(Array.from(newFileState.values())); + return commitFileStateChange(newFileState, patch); } function applyToFiles(fileIds: string[], callback: (file: GPXFile) => void) { const [newFileState, patch, inversePatch] = produceWithPatches(fileState, (draft) => { fileIds.forEach((fileId) => { - callback(draft.get(fileId)); + let file = draft.get(fileId); + if (file) { + callback(file); + } }); }); appendPatches(patch, inversePatch, false); - return updateFiles(fileIds.map((fileId) => newFileState.get(fileId))); + return commitFileStateChange(newFileState, patch); } -function appendPatches(patch: Patch[], inversePatch: Patch[], global: boolean) { - db.patches.where('id').above(patchIndex).delete(); - db.patches.add({ - patch, - inversePatch +async function appendPatches(patch: Patch[], inversePatch: Patch[], global: boolean) { + if (get(patchIndex) !== undefined) { + db.patches.where(':id').above(get(patchIndex)).delete(); + } + db.transaction('rw', db.patches, db.settings, async () => { + await db.patches.put({ + patch, + inversePatch + }, get(patchIndex) + 1); + await db.settings.put(get(patchIndex) + 1, 'patchIndex'); }); - db.settings.put(get(patchIndex) + 1, 'patchIndex'); } function applyPatch(patch: Patch[]) { let newFileState = applyPatches(fileState, patch); - let changedFiles = []; + return commitFileStateChange(newFileState, patch); +} + +function getChangedFileIds(patch: Patch[]) { + let changedFileIds = []; for (let p of patch) { let fileId = p.p?.toString(); if (fileId) { - let newFile = newFileState.get(fileId); - if (newFile) { - changedFiles.push(newFile); - } + changedFileIds.push(fileId); } } - return updateFiles(changedFiles); + return changedFileIds; } -function getFileId() { - for (let index = 0; ; index++) { +function getFileIds(n: number) { + let ids = []; + for (let index = 0; ids.length < n; index++) { let id = `gpx-${index}`; if (!get(fileObservers).has(id)) { - return id; + ids.push(id); } } + return ids; } export function undo() { @@ -141,17 +192,16 @@ export function redo() { export const dbUtils = { add: (file: GPXFile) => { - file._data.id = getFileId(); - console.log(file._data.id); - let result = applyGlobal((draft) => { + file._data.id = getFileIds(1)[0]; + return applyGlobal((draft) => { draft.set(file._data.id, file); }); - console.log(result); }, addMultiple: (files: GPXFile[]) => { - applyGlobal((draft) => { - files.forEach((file) => { - file._data.id = getFileId(); + return applyGlobal((draft) => { + let ids = getFileIds(files.length); + files.forEach((file, index) => { + file._data.id = ids[index]; draft.set(file._data.id, file); }); }); @@ -164,12 +214,13 @@ export const dbUtils = { }, duplicateSelectedFiles: () => { applyGlobal((draft) => { - get(fileOrder).forEach((fileId) => { + let ids = getFileIds(get(fileOrder).length); + get(fileOrder).forEach((fileId, index) => { if (get(selectedFiles).has(fileId)) { let file = draft.get(fileId); if (file) { let clone = file.clone(); - clone._data.id = getFileId(); + clone._data.id = ids[index]; draft.set(clone._data.id, clone); } } diff --git a/website/src/lib/stores.ts b/website/src/lib/stores.ts index 10b5af27..ed7c4154 100644 --- a/website/src/lib/stores.ts +++ b/website/src/lib/stores.ts @@ -5,7 +5,7 @@ import { GPXFile, buildGPX, parseGPX, GPXFiles } from 'gpx'; import { tick } from 'svelte'; import { _ } from 'svelte-i18n'; import type { GPXLayer } from '$lib/components/gpx-layer/GPXLayer'; -import { dbUtils, fileObservers, fileState } from './db'; +import { dbUtils, fileObservers } from './db'; export const map = writable(null); @@ -33,7 +33,10 @@ export const gpxData = writable(new GPXFiles([]).getTrackPointsAndStatistics()); function updateGPXData() { let fileIds: string[] = get(fileOrder).filter((f) => get(selectedFiles).has(f)); let files: GPXFile[] = fileIds - .map((id) => fileState.get(id)) + .map((id) => { + let fileObserver = get(fileObservers).get(id); + return fileObserver ? get(fileObserver) : null; + }) .filter((f) => f) as GPXFile[]; let gpxFiles = new GPXFiles(files); gpxData.set(gpxFiles.getTrackPointsAndStatistics()); @@ -82,8 +85,7 @@ export function createFile() { dbUtils.add(file); - tick().then(() => get(selectFiles).select(file._data.id)); - currentTool.set(Tool.ROUTING); + selectFileWhenLoaded(file._data.id); } export function triggerFileInput() { @@ -103,7 +105,7 @@ export function triggerFileInput() { export async function loadFiles(list: FileList) { let bounds = new mapboxgl.LngLatBounds(); let mapBounds = new mapboxgl.LngLatBounds([180, 90, -180, -90]); - if (fileState.size > 0) { + if (get(fileObservers).size > 0) { mapBounds = get(map)?.getBounds() ?? mapBounds; bounds.extend(mapBounds); } @@ -121,7 +123,10 @@ export async function loadFiles(list: FileList) { } } - dbUtils.addMultiple(files); + console.log('loadFiles', files); + + let result = dbUtils.addMultiple(files); + console.log('addMultiple', result); if (!mapBounds.contains(bounds.getSouthWest()) || !mapBounds.contains(bounds.getNorthEast()) || !mapBounds.contains(bounds.getSouthEast()) || !mapBounds.contains(bounds.getNorthWest())) { get(map)?.fitBounds(bounds, { @@ -131,11 +136,7 @@ export async function loadFiles(list: FileList) { }); } - await tick(); - - if (files.length > 0) { - get(selectFiles).select(files[0]._data.id); - } + selectFileWhenLoaded(files[0]._data.id); } export async function loadFile(file: File): Promise { @@ -158,20 +159,37 @@ export async function loadFile(file: File): Promise { return result; } -export async function exportSelectedFiles() { - for (let file of fileState.values()) { - if (get(selectedFiles).has(file._data.id)) { - exportFile(file); - await new Promise(resolve => setTimeout(resolve, 200)); +function selectFileWhenLoaded(fileId: string) { + const unsubscribe = fileObservers.subscribe((files) => { + if (files.has(fileId)) { + tick().then(() => { + get(selectFiles).select(fileId); + }); + unsubscribe(); } - } + }); } -export async function exportAllFiles() { - for (let file of fileState.values()) { - exportFile(file); - await new Promise(resolve => setTimeout(resolve, 200)); - } +export function exportSelectedFiles() { + get(fileObservers).forEach(async (file, fileId) => { + if (get(selectedFiles).has(fileId)) { + let f = get(file); + if (f) { + exportFile(f); + await new Promise(resolve => setTimeout(resolve, 200)); + } + } + }); +} + +export function exportAllFiles() { + get(fileObservers).forEach(async (file) => { + let f = get(file); + if (f) { + exportFile(f); + await new Promise(resolve => setTimeout(resolve, 200)); + } + }); } export function exportFile(file: GPXFile) {