dexie progress

This commit is contained in:
vcoppe
2024-05-03 15:59:34 +02:00
parent cd919258ad
commit 6c9faf54b1
10 changed files with 334 additions and 175 deletions

View File

@@ -163,12 +163,17 @@ export class GPXFile extends GPXTreeNode<Track>{
constructor(gpx?: GPXFileType | GPXFile) {
super();
if (gpx) {
this.attributes = cloneJSON(gpx.attributes);
this.metadata = cloneJSON(gpx.metadata);
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 instanceof GPXFile && gpx._data) {
this._data = cloneJSON(gpx._data);
}
if (gpx.hasOwnProperty('_data')) {
this._data = gpx._data;
}
} else {
this.attributes = {};
@@ -183,7 +188,13 @@ export class GPXFile extends GPXTreeNode<Track>{
}
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<TrackSegment> {
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;
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<TrackSegment> {
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<TrackSegment> {
extensions: this.extensions,
};
}
clone(): Track {
return new Track(this);
}
};
// A class that represents a TrackSegment in a GPX file
@@ -282,16 +307,28 @@ export class TrackSegment extends GPXTreeLeaf {
constructor(segment?: TrackSegmentType | TrackSegment) {
super();
if (segment) {
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 instanceof TrackSegment && segment._data) {
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 = [];
}
if (!this.statistics) {
this._computeStatistics();
}
}
_computeStatistics(): void {
let statistics = new GPXStatistics();
@@ -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 {

View File

@@ -123,14 +123,14 @@
});
</script>
<div class="h-10 -translate-y-10 w-full pointer-events-none">
<div class="h-10 -translate-y-10 w-full pointer-events-none absolute z-30">
<ScrollArea orientation="horizontal" class="w-full h-full" scrollbarXClasses="h-2">
<div bind:this={container} class="flex flex-row gap-1">
{#if $fileObservers}
{#each $fileObservers.values() as file}
{#each $fileObservers.entries() as [fileId, file]}
<div
bind:this={buttons[get(file)._data.id]}
data-id={get(file)._data.id}
bind:this={buttons[fileId]}
data-id={fileId}
class="pointer-events-auto first:ml-1 last:mr-1 mb-1 bg-transparent"
>
<FileListItem {file} />

View File

@@ -4,29 +4,32 @@
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<FreezedObject<GPXFile>> | undefined;
export let file: Readable<GPXFile | undefined>;
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if $file}
<div
on:contextmenu={() => {
if (!get(selectedFiles).has($file?._data.id)) {
get(selectFiles).select($file?._data.id);
if (!get(selectedFiles).has($file._data.id)) {
get(selectFiles).select($file._data.id);
}
}}
>
<ContextMenu.Root>
<ContextMenu.Trigger>
<Button variant="outline" class="h-9 px-1.5 py-1 border-none shadow-md">
{$file?.metadata.name}
<Button
variant="outline"
class="h-9 px-1.5 py-1 border-none shadow-md focus-visible:ring-0 focus-visible:ring-offset-0"
>
{$file.metadata.name}
</Button>
</ContextMenu.Trigger>
<ContextMenu.Content>
@@ -44,3 +47,4 @@
</ContextMenu.Content>
</ContextMenu.Root>
</div>
{/if}

View File

@@ -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);
</script>
<div class="absolute top-2 left-0 right-0 z-20 flex flex-row justify-center pointer-events-none">

View File

@@ -39,31 +39,36 @@ function decrementColor(color: string) {
export class GPXLayer {
map: mapboxgl.Map;
file: Readable<FreezedObject<GPXFile>>;
fileId: string;
file: Readable<FreezedObject<GPXFile> | 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<FreezedObject<GPXFile>>, popup: mapboxgl.Popup, popupElement: HTMLElement) {
constructor(map: mapboxgl.Map, fileId: string, file: Readable<FreezedObject<GPXFile> | 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() {
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,8 +152,10 @@ export class GPXLayer {
}
moveToFront() {
if (this.map.getLayer(this.fileId)) {
this.map.moveLayer(this.fileId);
}
}
selectOnClick(e: any) {
if (get(currentTool) === Tool.ROUTING) {
@@ -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 = {};

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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<GPXFile>;
fileId: string = '';
file: Readable<GPXFile | undefined>;
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<GPXFile>, popup: mapboxgl.Popup, popupElement: HTMLElement) {
constructor(map: mapboxgl.Map, fileId: string, file: Writable<GPXFile>, 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);
});

View File

@@ -6,49 +6,96 @@ import { fileOrder, selectedFiles } from './stores';
class Database extends Dexie {
fileids!: Dexie.Table<string, string>;
files!: Dexie.Table<FreezedObject<GPXFile>, string>;
patches!: Dexie.Table<{ patch: Patch[], inversePatch: Patch[] }, number>;
settings!: Dexie.Table<any, string>;
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<T>(querier: () => T | Promise<T>): Readable<T> {
const dexieObservable = liveQuery(querier)
function dexieFileStore(querier: () => FreezedObject<GPXFile> | undefined | Promise<FreezedObject<GPXFile> | undefined>): Readable<GPXFile> {
let store = writable<GPXFile>(undefined);
liveQuery(querier).subscribe(value => {
if (value !== undefined) {
let gpx = new GPXFile(value);
fileState.set(gpx._data.id, gpx);
store.set(gpx);
}
});
return {
subscribe(run, invalidate) {
return dexieObservable.subscribe(run, invalidate).unsubscribe
subscribe: store.subscribe,
};
}
function dexieStore<T>(querier: () => T | Promise<T>, initial?: T): Readable<T> {
let store = writable<T>(initial);
liveQuery(querier).subscribe(value => {
if (value !== undefined) {
store.set(value);
}
});
return {
subscribe: store.subscribe,
};
}
function updateFiles(files: (FreezedObject<GPXFile> | undefined)[], add: boolean = false) {
let filteredFiles = files.filter(file => file !== undefined) as FreezedObject<GPXFile>[];
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<GPXFile>[]) {
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<Map<string, Readable<FreezedObject<GPXFile>>>> = writable(new Map());
export const fileState: Map<string, FreezedObject<GPXFile>> = new Map(); // Used to generate patches
function commitFileStateChange(newFileState: ReadonlyMap<string, FreezedObject<GPXFile>>, 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<Map<string, Readable<GPXFile | undefined>>> = writable(new Map());
const fileState: Map<string, GPXFile> = 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<number> = dexieStore(() => db.settings.get('patchIndex'), -1);
const patches: Readable<{ patch: Patch[], inversePatch: Patch[] }[]> = dexieStore(() => db.patches.toArray(), []);
export const canUndo: Readable<boolean> = derived(patchIndex, ($patchIndex) => $patchIndex >= 0);
export const canRedo: Readable<boolean> = derived([patchIndex, patches], ([$patchIndex, $patches]) => $patchIndex < $patches.length - 1);
export function applyGlobal(callback: (files: Map<string, GPXFile>) => 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({
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);
}
}

View File

@@ -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<mapboxgl.Map | null>(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<GPXFile | null> {
@@ -158,21 +159,38 @@ export async function loadFile(file: File): Promise<GPXFile | null> {
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);
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) {
let blob = new Blob([buildGPX(file)], { type: 'application/gpx+xml' });