diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index fee272c2..56328e01 100644 --- a/gpx/src/gpx.ts +++ b/gpx/src/gpx.ts @@ -1,5 +1,5 @@ import { Coordinates, GPXFileAttributes, GPXFileType, Link, Metadata, TrackExtensions, TrackPointExtensions, TrackPointType, TrackSegmentType, TrackType, WaypointType } from "./types"; -import { immerable } from "immer"; +import { Draft, castDraft, immerable, produce } from "immer"; function cloneJSON(obj: T): T { if (obj === null || typeof obj !== 'object') { @@ -13,18 +13,18 @@ export abstract class GPXTreeElement> { _data: { [key: string]: any } = {}; abstract isLeaf(): boolean; - abstract getChildren(): T[]; - - abstract append(points: TrackPoint[]): void; - abstract reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date): void; + abstract getChildren(): ReadonlyArray; abstract getStartTimestamp(): Date; abstract getEndTimestamp(): Date; - abstract getTrackPoints(): TrackPoint[]; abstract getStatistics(): GPXStatistics; abstract getSegments(): TrackSegment[]; abstract toGeoJSON(): GeoJSON.Feature | GeoJSON.Feature[] | GeoJSON.FeatureCollection | GeoJSON.FeatureCollection[]; + + // Producers + abstract replace(segment: number, start: number, end: number, points: TrackPoint[]); + abstract reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date); } export type AnyGPXTreeElement = GPXTreeElement>; @@ -35,36 +35,6 @@ abstract class GPXTreeNode> extends GPXTreeElement return false; } - append(points: TrackPoint[]): void { - let children = this.getChildren(); - - if (children.length === 0) { - return; - } - - children[children.length - 1].append(points); - } - - reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date): void { - const children = this.getChildren(); - - if (!originalNextTimestamp && !newPreviousTimestamp) { - originalNextTimestamp = children[children.length - 1].getEndTimestamp(); - newPreviousTimestamp = children[0].getStartTimestamp(); - } - - children.reverse(); - - for (let i = 0; i < children.length; i++) { - let originalStartTimestamp = children[i].getStartTimestamp(); - - children[i].reverse(originalNextTimestamp, newPreviousTimestamp); - - originalNextTimestamp = originalStartTimestamp; - newPreviousTimestamp = children[i].getEndTimestamp(); - } - } - getStartTimestamp(): Date { return this.getChildren()[0].getStartTimestamp(); } @@ -73,10 +43,6 @@ abstract class GPXTreeNode> extends GPXTreeElement return this.getChildren()[this.getChildren().length - 1].getEndTimestamp(); } - getTrackPoints(): TrackPoint[] { - return this.getChildren().flatMap((child) => child.getTrackPoints()); - } - getStatistics(): GPXStatistics { let statistics = new GPXStatistics(); for (let child of this.getChildren()) { @@ -88,6 +54,44 @@ abstract class GPXTreeNode> extends GPXTreeElement getSegments(): TrackSegment[] { return this.getChildren().flatMap((child) => child.getSegments()); } + + // Producers + replace(segment: number, start: number, end: number, points: TrackPoint[]) { + return produce(this, (draft: Draft>) => { + let children = castDraft(draft.getChildren()); + let cumul = 0; + for (let i = 0; i < children.length; i++) { + let childSegments = children[i].getSegments(); + if (segment < cumul + childSegments.length) { + children[i] = children[i].replace(segment - cumul, start, end, points); + break; + } + cumul += childSegments.length; + } + }); + } + + reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date) { + return produce(this, (draft: Draft>) => { + const children = castDraft(draft.getChildren()); + + if (!originalNextTimestamp && !newPreviousTimestamp) { + originalNextTimestamp = children[children.length - 1].getEndTimestamp(); + newPreviousTimestamp = children[0].getStartTimestamp(); + } + + children.reverse(); + + for (let i = 0; i < children.length; i++) { + let originalStartTimestamp = children[i].getStartTimestamp(); + + children[i] = children[i].reverse(originalNextTimestamp, newPreviousTimestamp); + + originalNextTimestamp = originalStartTimestamp; + newPreviousTimestamp = children[i].getEndTimestamp(); + } + }); + } } // An abstract class that TrackSegment extends to implement the GPXTreeElement interface @@ -96,21 +100,21 @@ abstract class GPXTreeLeaf extends GPXTreeElement { return true; } - getChildren(): GPXTreeLeaf[] { + getChildren(): ReadonlyArray { return []; } } // A class that represents a set of GPX files export class GPXFiles extends GPXTreeNode { - files: GPXFile[]; + readonly files: ReadonlyArray; constructor(files: GPXFile[]) { super(); this.files = files; } - getChildren(): GPXFile[] { + getChildren(): ReadonlyArray { return this.files; } @@ -123,12 +127,12 @@ export class GPXFiles extends GPXTreeNode { export class GPXFile extends GPXTreeNode{ [immerable] = true; - attributes: GPXFileAttributes; - metadata: Metadata; - wpt: Waypoint[]; - trk: Track[]; + readonly attributes: GPXFileAttributes; + readonly metadata: Metadata; + readonly wpt: ReadonlyArray>; + readonly trk: ReadonlyArray; - constructor(gpx?: GPXFileType | GPXFile) { + constructor(gpx?: GPXFileType & { _data?: any } | GPXFile) { super(); if (gpx) { this.attributes = gpx.attributes @@ -146,7 +150,7 @@ export class GPXFile extends GPXTreeNode{ } } - getChildren(): Track[] { + getChildren(): ReadonlyArray { return this.trk; } @@ -181,16 +185,16 @@ export class GPXFile extends GPXTreeNode{ export class Track extends GPXTreeNode { [immerable] = true; - name?: string; - cmt?: string; - desc?: string; - src?: string; - link?: Link; - type?: string; - trkseg: TrackSegment[]; - extensions?: TrackExtensions; + readonly name?: string; + readonly cmt?: string; + readonly desc?: string; + readonly src?: string; + readonly link?: Link; + readonly type?: string; + readonly trkseg: ReadonlyArray; + readonly extensions?: TrackExtensions; - constructor(track?: TrackType | Track) { + constructor(track?: TrackType & { _data?: any } | Track) { super(); if (track) { this.name = track.name; @@ -209,7 +213,7 @@ export class Track extends GPXTreeNode { } } - getChildren(): TrackSegment[] { + getChildren(): ReadonlyArray { return this.trkseg; } @@ -263,9 +267,9 @@ export class Track extends GPXTreeNode { export class TrackSegment extends GPXTreeLeaf { [immerable] = true; - trkpt: TrackPoint[]; + readonly trkpt: ReadonlyArray>; - constructor(segment?: TrackSegmentType | TrackSegment) { + constructor(segment?: TrackSegmentType & { _data?: any } | TrackSegment) { super(); if (segment) { this.trkpt = segment.trkpt.map((point) => new TrackPoint(point)); @@ -280,7 +284,7 @@ export class TrackSegment extends GPXTreeLeaf { _computeStatistics(): GPXStatistics { let statistics = new GPXStatistics(); - statistics.local.points = this.trkpt; + statistics.local.points = this.trkpt.map((point) => point); statistics.local.elevation.smoothed = this._computeSmoothedElevation(); statistics.local.slope = this._computeSlope(); @@ -366,33 +370,6 @@ export class TrackSegment extends GPXTreeLeaf { return distanceWindowSmoothingWithDistanceAccumulator(points, 50, (accumulated, start, end) => 100 * (points[end].ele - points[start].ele) / accumulated); } - append(points: TrackPoint[]): void { - this.trkpt = this.trkpt.concat(points); - } - - replace(start: number, end: number, points: TrackPoint[]): void { - this.trkpt.splice(start, end - start + 1, ...points); - } - - reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date): void { - if (originalNextTimestamp !== undefined && newPreviousTimestamp !== undefined) { - let originalEndTimestamp = this.getEndTimestamp(); - let newStartTimestamp = new Date( - newPreviousTimestamp.getTime() + originalNextTimestamp.getTime() - originalEndTimestamp.getTime() - ); - - this.trkpt.reverse(); - - for (let i = 0; i < this.trkpt.length; i++) { - this.trkpt[i].time = new Date( - newStartTimestamp.getTime() + (originalEndTimestamp.getTime() - this.trkpt[i].time.getTime()) - ); - } - } else { - this.trkpt.reverse(); - } - } - getStartTimestamp(): Date { return this.trkpt[0].time; } @@ -401,10 +378,6 @@ export class TrackSegment extends GPXTreeLeaf { return this.trkpt[this.trkpt.length - 1].time; } - getTrackPoints(): TrackPoint[] { - return this.trkpt; - } - getStatistics(): GPXStatistics { return this._computeStatistics(); } @@ -436,6 +409,31 @@ export class TrackSegment extends GPXTreeLeaf { _data: cloneJSON(this._data), }); } + + // Producers + replace(segment: number, start: number, end: number, points: TrackPoint[]) { + return produce(this, (draft) => { + draft.trkpt.splice(start, end - start + 1, ...points); + }); + } + + reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date) { + return produce(this, (draft) => { + if (originalNextTimestamp !== undefined && newPreviousTimestamp !== undefined) { + let originalEndTimestamp = draft.getEndTimestamp(); + let newStartTimestamp = new Date( + newPreviousTimestamp.getTime() + originalNextTimestamp.getTime() - originalEndTimestamp.getTime() + ); + + for (let i = 0; i < draft.trkpt.length; i++) { + draft.trkpt[i].time = new Date( + newStartTimestamp.getTime() + (originalEndTimestamp.getTime() - draft.trkpt[i].time.getTime()) + ); + } + } + draft.trkpt.reverse(); + }); + } }; export class TrackPoint { @@ -447,7 +445,7 @@ export class TrackPoint { extensions?: TrackPointExtensions; _data: { [key: string]: any } = {}; - constructor(point: TrackPointType | TrackPoint) { + constructor(point: TrackPointType & { _data?: any } | TrackPoint) { this.attributes = point.attributes; this.ele = point.ele; this.time = point.time; @@ -701,7 +699,7 @@ export function distance(coord1: Coordinates, coord2: Coordinates): number { return maxMeters; } -function distanceWindowSmoothing(points: TrackPoint[], distanceWindow: number, accumulate: (index: number) => number, compute: (accumulated: number, start: number, end: number) => number, remove?: (index: number) => number): number[] { +function distanceWindowSmoothing(points: ReadonlyArray>, distanceWindow: number, accumulate: (index: number) => number, compute: (accumulated: number, start: number, end: number) => number, remove?: (index: number) => number): number[] { let result = []; let start = 0, end = 0, accumulated = 0; @@ -724,6 +722,6 @@ function distanceWindowSmoothing(points: TrackPoint[], distanceWindow: number, a return result; } -function distanceWindowSmoothingWithDistanceAccumulator(points: TrackPoint[], distanceWindow: number, compute: (accumulated: number, start: number, end: number) => number): number[] { +function distanceWindowSmoothingWithDistanceAccumulator(points: ReadonlyArray>, distanceWindow: number, compute: (accumulated: number, start: number, end: number) => number): number[] { return distanceWindowSmoothing(points, distanceWindow, (index) => index > 0 ? distance(points[index - 1].getCoordinates(), points[index].getCoordinates()) : 0, compute, (index) => distance(points[index].getCoordinates(), points[index + 1].getCoordinates())); } \ No newline at end of file diff --git a/gpx/src/types.ts b/gpx/src/types.ts index 05033174..a6ab6fc0 100644 --- a/gpx/src/types.ts +++ b/gpx/src/types.ts @@ -1,8 +1,8 @@ export type GPXFileType = { attributes: GPXFileAttributes; metadata: Metadata; - wpt: WaypointType[]; - trk: TrackType[]; + wpt: ReadonlyArray; + trk: ReadonlyArray; }; export type GPXFileAttributes = { @@ -52,7 +52,7 @@ export type TrackType = { src?: string; link?: Link; type?: string; - trkseg: TrackSegmentType[]; + trkseg: ReadonlyArray; extensions?: TrackExtensions; }; @@ -67,7 +67,7 @@ export type LineStyleExtension = { }; export type TrackSegmentType = { - trkpt: TrackPointType[]; + trkpt: ReadonlyArray; }; export type TrackPointType = { diff --git a/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts b/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts index e822136f..527f8914 100644 --- a/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts +++ b/website/src/lib/components/toolbar/tools/routing/RoutingControls.ts @@ -1,4 +1,5 @@ import { distance, type Coordinates, TrackPoint, TrackSegment } from "gpx"; +import { original } from "immer"; import { get, type Readable } from "svelte/store"; import { computeAnchorPoints } from "./Simplify"; import mapboxgl from "mapbox-gl"; @@ -297,19 +298,13 @@ export class RoutingControls { let [previousAnchor, nextAnchor] = this.getNeighbouringAnchors(anchor); if (previousAnchor === null && nextAnchor === null) { // Only one point, remove it - dbUtils.applyToFile(this.fileId, (file) => { - let segment = file.getSegments()[anchor.segmentIndex]; - segment.replace(0, 0, []); - }); + dbUtils.applyToFile(this.fileId, (file) => file.replace(anchor.segmentIndex, 0, 0, [])); } else if (previousAnchor === null) { // First point, remove trackpoints until nextAnchor - dbUtils.applyToFile(this.fileId, (file) => { - let segment = file.getSegments()[anchor.segmentIndex]; - segment.replace(0, nextAnchor.point._data.index - 1, []); - }); + dbUtils.applyToFile(this.fileId, (file) => file.replace(anchor.segmentIndex, 0, nextAnchor.point._data.index - 1, [])); } else if (nextAnchor === null) { // Last point, remove trackpoints from previousAnchor dbUtils.applyToFile(this.fileId, (file) => { let segment = file.getSegments()[anchor.segmentIndex]; - segment.replace(previousAnchor.point._data.index + 1, segment.trkpt.length - 1, []); + return file.replace(anchor.segmentIndex, previousAnchor.point._data.index + 1, segment.trkpt.length - 1, []); }); } else { // Route between previousAnchor and nextAnchor this.routeBetweenAnchors([previousAnchor, nextAnchor], [previousAnchor.point.getCoordinates(), nextAnchor.point.getCoordinates()]); @@ -334,10 +329,7 @@ export class RoutingControls { if (!lastAnchor) { // TODO, create segment if it does not exist - dbUtils.applyToFile(this.fileId, (file) => { - let segment = file.getSegments()[0]; - segment.replace(0, 0, [newPoint]); - }); + dbUtils.applyToFile(this.fileId, (file) => file.replace(0, 0, 0, [newPoint])); return; } @@ -384,10 +376,10 @@ export class RoutingControls { } dbUtils.applyToFile(this.fileId, (file) => { - let segment = file.getSegments()[segments.length - 1]; + let segment = original(file).getSegments()[segments.length - 1]; let newSegment = segment.clone(); - newSegment.reverse(undefined, undefined); - segment.replace(segment.trkpt.length, segment.trkpt.length, newSegment.trkpt); + newSegment = newSegment.reverse(segment.getEndTimestamp(), segment.getEndTimestamp()); + return file.replace(segments.length - 1, segment.trkpt.length, segment.trkpt.length, newSegment.trkpt.map((point) => point)); }); } @@ -416,12 +408,9 @@ export class RoutingControls { let segment = anchors[0].segment; if (anchors.length === 1) { // Only one anchor, update the point in the segment - dbUtils.applyToFile(this.fileId, (file) => { - let segment = file.getSegments()[anchors[0].segmentIndex]; - segment.replace(0, 0, [new TrackPoint({ - attributes: targetCoordinates[0], - })]); - }); + dbUtils.applyToFile(this.fileId, (file) => file.replace(anchors[0].segmentIndex, 0, 0, [new TrackPoint({ + attributes: targetCoordinates[0], + })])); return true; } @@ -476,10 +465,7 @@ export class RoutingControls { anchor.point._data.zoom = 0; // Make these anchors permanent }); - dbUtils.applyToFile(this.fileId, (file) => { - let segment = file.getSegments()[anchors[0].segmentIndex]; - segment.replace(start, end, response); - }); + dbUtils.applyToFile(this.fileId, (file) => file.replace(anchors[0].segmentIndex, start, end, response)); return true; } diff --git a/website/src/lib/db.ts b/website/src/lib/db.ts index 314256ee..346f2f5d 100644 --- a/website/src/lib/db.ts +++ b/website/src/lib/db.ts @@ -1,6 +1,6 @@ import Dexie, { liveQuery } from 'dexie'; import { GPXFile, GPXStatistics } from 'gpx'; -import { enableMapSet, enablePatches, produceWithPatches, applyPatches, type Patch } from 'immer'; +import { enableMapSet, enablePatches, produceWithPatches, applyPatches, type Patch, type WritableDraft, castDraft } from 'immer'; import { writable, get, derived, type Readable, type Writable } from 'svelte/store'; import { fileOrder, initTargetMapBounds, selectedFiles, updateTargetMapBounds } from './stores'; import { mode } from 'mode-watcher'; @@ -218,12 +218,12 @@ function applyGlobal(callback: (files: Map) => void) { } // Helper function to apply a callback to multiple files -function applyToFiles(fileIds: string[], callback: (file: GPXFile) => void) { +function applyToFiles(fileIds: string[], callback: (file: WritableDraft) => GPXFile) { const [newFileState, patch, inversePatch] = produceWithPatches(fileState, (draft) => { fileIds.forEach((fileId) => { let file = draft.get(fileId); if (file) { - callback(file); + draft.set(fileId, castDraft(callback(file))); } }); }); @@ -298,10 +298,10 @@ export const dbUtils = { }); }); }, - applyToFile: (id: string, callback: (file: GPXFile) => void) => { + applyToFile: (id: string, callback: (file: WritableDraft) => GPXFile) => { applyToFiles([id], callback); }, - applyToSelectedFiles: (callback: (file: GPXFile) => void) => { + applyToSelectedFiles: (callback: (file: WritableDraft) => GPXFile) => { applyToFiles(get(fileOrder).filter(fileId => get(selectedFiles).has(fileId)), callback); }, duplicateSelectedFiles: () => { diff --git a/website/src/lib/stores.ts b/website/src/lib/stores.ts index 4edc7c6a..0b37889e 100644 --- a/website/src/lib/stores.ts +++ b/website/src/lib/stores.ts @@ -203,7 +203,7 @@ export function exportSelectedFiles() { if (get(selectedFiles).has(fileId)) { let f = get(file); if (f) { - exportFile(f); + exportFile(f.file); await new Promise(resolve => setTimeout(resolve, 200)); } } @@ -214,7 +214,7 @@ export function exportAllFiles() { get(fileObservers).forEach(async (file) => { let f = get(file); if (f) { - exportFile(f); + exportFile(f.file); await new Promise(resolve => setTimeout(resolve, 200)); } });