mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-31 23:53:25 +00:00
dexie progress
This commit is contained in:
117
gpx/src/gpx.ts
117
gpx/src/gpx.ts
@@ -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.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<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;
|
||||
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<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,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 {
|
||||
|
@@ -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} />
|
||||
|
@@ -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<FreezedObject<GPXFile>> | undefined;
|
||||
export let file: Readable<GPXFile | undefined>;
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
on:contextmenu={() => {
|
||||
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>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item on:click={dbUtils.duplicateSelectedFiles}>
|
||||
<Copy size="16" class="mr-1" />
|
||||
{$_('menu.duplicate')}
|
||||
<Shortcut key="D" ctrl={true} /></ContextMenu.Item
|
||||
>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item on:click={dbUtils.deleteSelectedFiles}
|
||||
><Trash2 size="16" class="mr-1" />
|
||||
{$_('menu.delete')}
|
||||
<Shortcut key="⌫" ctrl={true} /></ContextMenu.Item
|
||||
>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Root>
|
||||
</div>
|
||||
{#if $file}
|
||||
<div
|
||||
on:contextmenu={() => {
|
||||
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 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
>
|
||||
{$file.metadata.name}
|
||||
</Button>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item on:click={dbUtils.duplicateSelectedFiles}>
|
||||
<Copy size="16" class="mr-1" />
|
||||
{$_('menu.duplicate')}
|
||||
<Shortcut key="D" ctrl={true} /></ContextMenu.Item
|
||||
>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item on:click={dbUtils.deleteSelectedFiles}
|
||||
><Trash2 size="16" class="mr-1" />
|
||||
{$_('menu.delete')}
|
||||
<Shortcut key="⌫" ctrl={true} /></ContextMenu.Item
|
||||
>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Root>
|
||||
</div>
|
||||
{/if}
|
||||
|
@@ -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">
|
||||
|
@@ -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() {
|
||||
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 = {};
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
@@ -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);
|
||||
});
|
||||
|
@@ -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)
|
||||
return {
|
||||
subscribe(run, invalidate) {
|
||||
return dexieObservable.subscribe(run, invalidate).unsubscribe
|
||||
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: 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({
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@@ -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,20 +159,37 @@ 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);
|
||||
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) {
|
||||
|
Reference in New Issue
Block a user