better use immer

This commit is contained in:
vcoppe
2024-05-15 15:30:02 +02:00
parent d93e43e268
commit 93a3a28182
5 changed files with 117 additions and 133 deletions

View File

@@ -1,5 +1,5 @@
import { Coordinates, GPXFileAttributes, GPXFileType, Link, Metadata, TrackExtensions, TrackPointExtensions, TrackPointType, TrackSegmentType, TrackType, WaypointType } from "./types"; 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<T>(obj: T): T { function cloneJSON<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') { if (obj === null || typeof obj !== 'object') {
@@ -13,18 +13,18 @@ export abstract class GPXTreeElement<T extends GPXTreeElement<any>> {
_data: { [key: string]: any } = {}; _data: { [key: string]: any } = {};
abstract isLeaf(): boolean; abstract isLeaf(): boolean;
abstract getChildren(): T[]; abstract getChildren(): ReadonlyArray<T>;
abstract append(points: TrackPoint[]): void;
abstract reverse(originalNextTimestamp?: Date, newPreviousTimestamp?: Date): void;
abstract getStartTimestamp(): Date; abstract getStartTimestamp(): Date;
abstract getEndTimestamp(): Date; abstract getEndTimestamp(): Date;
abstract getTrackPoints(): TrackPoint[];
abstract getStatistics(): GPXStatistics; abstract getStatistics(): GPXStatistics;
abstract getSegments(): TrackSegment[]; abstract getSegments(): TrackSegment[];
abstract toGeoJSON(): GeoJSON.Feature | GeoJSON.Feature[] | GeoJSON.FeatureCollection | GeoJSON.FeatureCollection[]; 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<GPXTreeElement<any>>; export type AnyGPXTreeElement = GPXTreeElement<GPXTreeElement<any>>;
@@ -35,36 +35,6 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
return false; 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 { getStartTimestamp(): Date {
return this.getChildren()[0].getStartTimestamp(); return this.getChildren()[0].getStartTimestamp();
} }
@@ -73,10 +43,6 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
return this.getChildren()[this.getChildren().length - 1].getEndTimestamp(); return this.getChildren()[this.getChildren().length - 1].getEndTimestamp();
} }
getTrackPoints(): TrackPoint[] {
return this.getChildren().flatMap((child) => child.getTrackPoints());
}
getStatistics(): GPXStatistics { getStatistics(): GPXStatistics {
let statistics = new GPXStatistics(); let statistics = new GPXStatistics();
for (let child of this.getChildren()) { for (let child of this.getChildren()) {
@@ -88,6 +54,44 @@ abstract class GPXTreeNode<T extends GPXTreeElement<any>> extends GPXTreeElement
getSegments(): TrackSegment[] { getSegments(): TrackSegment[] {
return this.getChildren().flatMap((child) => child.getSegments()); return this.getChildren().flatMap((child) => child.getSegments());
} }
// Producers
replace(segment: number, start: number, end: number, points: TrackPoint[]) {
return produce(this, (draft: Draft<GPXTreeNode<T>>) => {
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<GPXTreeNode<T>>) => {
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 // An abstract class that TrackSegment extends to implement the GPXTreeElement interface
@@ -96,21 +100,21 @@ abstract class GPXTreeLeaf extends GPXTreeElement<GPXTreeLeaf> {
return true; return true;
} }
getChildren(): GPXTreeLeaf[] { getChildren(): ReadonlyArray<GPXTreeLeaf> {
return []; return [];
} }
} }
// A class that represents a set of GPX files // A class that represents a set of GPX files
export class GPXFiles extends GPXTreeNode<GPXFile> { export class GPXFiles extends GPXTreeNode<GPXFile> {
files: GPXFile[]; readonly files: ReadonlyArray<GPXFile>;
constructor(files: GPXFile[]) { constructor(files: GPXFile[]) {
super(); super();
this.files = files; this.files = files;
} }
getChildren(): GPXFile[] { getChildren(): ReadonlyArray<GPXFile> {
return this.files; return this.files;
} }
@@ -123,12 +127,12 @@ export class GPXFiles extends GPXTreeNode<GPXFile> {
export class GPXFile extends GPXTreeNode<Track>{ export class GPXFile extends GPXTreeNode<Track>{
[immerable] = true; [immerable] = true;
attributes: GPXFileAttributes; readonly attributes: GPXFileAttributes;
metadata: Metadata; readonly metadata: Metadata;
wpt: Waypoint[]; readonly wpt: ReadonlyArray<Readonly<Waypoint>>;
trk: Track[]; readonly trk: ReadonlyArray<Track>;
constructor(gpx?: GPXFileType | GPXFile) { constructor(gpx?: GPXFileType & { _data?: any } | GPXFile) {
super(); super();
if (gpx) { if (gpx) {
this.attributes = gpx.attributes this.attributes = gpx.attributes
@@ -146,7 +150,7 @@ export class GPXFile extends GPXTreeNode<Track>{
} }
} }
getChildren(): Track[] { getChildren(): ReadonlyArray<Track> {
return this.trk; return this.trk;
} }
@@ -181,16 +185,16 @@ export class GPXFile extends GPXTreeNode<Track>{
export class Track extends GPXTreeNode<TrackSegment> { export class Track extends GPXTreeNode<TrackSegment> {
[immerable] = true; [immerable] = true;
name?: string; readonly name?: string;
cmt?: string; readonly cmt?: string;
desc?: string; readonly desc?: string;
src?: string; readonly src?: string;
link?: Link; readonly link?: Link;
type?: string; readonly type?: string;
trkseg: TrackSegment[]; readonly trkseg: ReadonlyArray<TrackSegment>;
extensions?: TrackExtensions; readonly extensions?: TrackExtensions;
constructor(track?: TrackType | Track) { constructor(track?: TrackType & { _data?: any } | Track) {
super(); super();
if (track) { if (track) {
this.name = track.name; this.name = track.name;
@@ -209,7 +213,7 @@ export class Track extends GPXTreeNode<TrackSegment> {
} }
} }
getChildren(): TrackSegment[] { getChildren(): ReadonlyArray<TrackSegment> {
return this.trkseg; return this.trkseg;
} }
@@ -263,9 +267,9 @@ export class Track extends GPXTreeNode<TrackSegment> {
export class TrackSegment extends GPXTreeLeaf { export class TrackSegment extends GPXTreeLeaf {
[immerable] = true; [immerable] = true;
trkpt: TrackPoint[]; readonly trkpt: ReadonlyArray<Readonly<TrackPoint>>;
constructor(segment?: TrackSegmentType | TrackSegment) { constructor(segment?: TrackSegmentType & { _data?: any } | TrackSegment) {
super(); super();
if (segment) { if (segment) {
this.trkpt = segment.trkpt.map((point) => new TrackPoint(point)); this.trkpt = segment.trkpt.map((point) => new TrackPoint(point));
@@ -280,7 +284,7 @@ export class TrackSegment extends GPXTreeLeaf {
_computeStatistics(): GPXStatistics { _computeStatistics(): GPXStatistics {
let statistics = new 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.elevation.smoothed = this._computeSmoothedElevation();
statistics.local.slope = this._computeSlope(); 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); 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 { getStartTimestamp(): Date {
return this.trkpt[0].time; return this.trkpt[0].time;
} }
@@ -401,10 +378,6 @@ export class TrackSegment extends GPXTreeLeaf {
return this.trkpt[this.trkpt.length - 1].time; return this.trkpt[this.trkpt.length - 1].time;
} }
getTrackPoints(): TrackPoint[] {
return this.trkpt;
}
getStatistics(): GPXStatistics { getStatistics(): GPXStatistics {
return this._computeStatistics(); return this._computeStatistics();
} }
@@ -436,6 +409,31 @@ export class TrackSegment extends GPXTreeLeaf {
_data: cloneJSON(this._data), _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 { export class TrackPoint {
@@ -447,7 +445,7 @@ export class TrackPoint {
extensions?: TrackPointExtensions; extensions?: TrackPointExtensions;
_data: { [key: string]: any } = {}; _data: { [key: string]: any } = {};
constructor(point: TrackPointType | TrackPoint) { constructor(point: TrackPointType & { _data?: any } | TrackPoint) {
this.attributes = point.attributes; this.attributes = point.attributes;
this.ele = point.ele; this.ele = point.ele;
this.time = point.time; this.time = point.time;
@@ -701,7 +699,7 @@ export function distance(coord1: Coordinates, coord2: Coordinates): number {
return maxMeters; 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<Readonly<TrackPoint>>, distanceWindow: number, accumulate: (index: number) => number, compute: (accumulated: number, start: number, end: number) => number, remove?: (index: number) => number): number[] {
let result = []; let result = [];
let start = 0, end = 0, accumulated = 0; let start = 0, end = 0, accumulated = 0;
@@ -724,6 +722,6 @@ function distanceWindowSmoothing(points: TrackPoint[], distanceWindow: number, a
return result; return result;
} }
function distanceWindowSmoothingWithDistanceAccumulator(points: TrackPoint[], distanceWindow: number, compute: (accumulated: number, start: number, end: number) => number): number[] { function distanceWindowSmoothingWithDistanceAccumulator(points: ReadonlyArray<Readonly<TrackPoint>>, 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())); 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()));
} }

View File

@@ -1,8 +1,8 @@
export type GPXFileType = { export type GPXFileType = {
attributes: GPXFileAttributes; attributes: GPXFileAttributes;
metadata: Metadata; metadata: Metadata;
wpt: WaypointType[]; wpt: ReadonlyArray<WaypointType>;
trk: TrackType[]; trk: ReadonlyArray<TrackType>;
}; };
export type GPXFileAttributes = { export type GPXFileAttributes = {
@@ -52,7 +52,7 @@ export type TrackType = {
src?: string; src?: string;
link?: Link; link?: Link;
type?: string; type?: string;
trkseg: TrackSegmentType[]; trkseg: ReadonlyArray<TrackSegmentType>;
extensions?: TrackExtensions; extensions?: TrackExtensions;
}; };
@@ -67,7 +67,7 @@ export type LineStyleExtension = {
}; };
export type TrackSegmentType = { export type TrackSegmentType = {
trkpt: TrackPointType[]; trkpt: ReadonlyArray<TrackPointType>;
}; };
export type TrackPointType = { export type TrackPointType = {

View File

@@ -1,4 +1,5 @@
import { distance, type Coordinates, TrackPoint, TrackSegment } from "gpx"; import { distance, type Coordinates, TrackPoint, TrackSegment } from "gpx";
import { original } from "immer";
import { get, type Readable } from "svelte/store"; import { get, type Readable } from "svelte/store";
import { computeAnchorPoints } from "./Simplify"; import { computeAnchorPoints } from "./Simplify";
import mapboxgl from "mapbox-gl"; import mapboxgl from "mapbox-gl";
@@ -297,19 +298,13 @@ export class RoutingControls {
let [previousAnchor, nextAnchor] = this.getNeighbouringAnchors(anchor); let [previousAnchor, nextAnchor] = this.getNeighbouringAnchors(anchor);
if (previousAnchor === null && nextAnchor === null) { // Only one point, remove it if (previousAnchor === null && nextAnchor === null) { // Only one point, remove it
dbUtils.applyToFile(this.fileId, (file) => { dbUtils.applyToFile(this.fileId, (file) => file.replace(anchor.segmentIndex, 0, 0, []));
let segment = file.getSegments()[anchor.segmentIndex];
segment.replace(0, 0, []);
});
} else if (previousAnchor === null) { // First point, remove trackpoints until nextAnchor } else if (previousAnchor === null) { // First point, remove trackpoints until nextAnchor
dbUtils.applyToFile(this.fileId, (file) => { dbUtils.applyToFile(this.fileId, (file) => file.replace(anchor.segmentIndex, 0, nextAnchor.point._data.index - 1, []));
let segment = file.getSegments()[anchor.segmentIndex];
segment.replace(0, nextAnchor.point._data.index - 1, []);
});
} else if (nextAnchor === null) { // Last point, remove trackpoints from previousAnchor } else if (nextAnchor === null) { // Last point, remove trackpoints from previousAnchor
dbUtils.applyToFile(this.fileId, (file) => { dbUtils.applyToFile(this.fileId, (file) => {
let segment = file.getSegments()[anchor.segmentIndex]; 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 } else { // Route between previousAnchor and nextAnchor
this.routeBetweenAnchors([previousAnchor, nextAnchor], [previousAnchor.point.getCoordinates(), nextAnchor.point.getCoordinates()]); this.routeBetweenAnchors([previousAnchor, nextAnchor], [previousAnchor.point.getCoordinates(), nextAnchor.point.getCoordinates()]);
@@ -334,10 +329,7 @@ export class RoutingControls {
if (!lastAnchor) { if (!lastAnchor) {
// TODO, create segment if it does not exist // TODO, create segment if it does not exist
dbUtils.applyToFile(this.fileId, (file) => { dbUtils.applyToFile(this.fileId, (file) => file.replace(0, 0, 0, [newPoint]));
let segment = file.getSegments()[0];
segment.replace(0, 0, [newPoint]);
});
return; return;
} }
@@ -384,10 +376,10 @@ export class RoutingControls {
} }
dbUtils.applyToFile(this.fileId, (file) => { dbUtils.applyToFile(this.fileId, (file) => {
let segment = file.getSegments()[segments.length - 1]; let segment = original(file).getSegments()[segments.length - 1];
let newSegment = segment.clone(); let newSegment = segment.clone();
newSegment.reverse(undefined, undefined); newSegment = newSegment.reverse(segment.getEndTimestamp(), segment.getEndTimestamp());
segment.replace(segment.trkpt.length, segment.trkpt.length, newSegment.trkpt); 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; let segment = anchors[0].segment;
if (anchors.length === 1) { // Only one anchor, update the point in the segment if (anchors.length === 1) { // Only one anchor, update the point in the segment
dbUtils.applyToFile(this.fileId, (file) => { dbUtils.applyToFile(this.fileId, (file) => file.replace(anchors[0].segmentIndex, 0, 0, [new TrackPoint({
let segment = file.getSegments()[anchors[0].segmentIndex]; attributes: targetCoordinates[0],
segment.replace(0, 0, [new TrackPoint({ })]));
attributes: targetCoordinates[0],
})]);
});
return true; return true;
} }
@@ -476,10 +465,7 @@ export class RoutingControls {
anchor.point._data.zoom = 0; // Make these anchors permanent anchor.point._data.zoom = 0; // Make these anchors permanent
}); });
dbUtils.applyToFile(this.fileId, (file) => { dbUtils.applyToFile(this.fileId, (file) => file.replace(anchors[0].segmentIndex, start, end, response));
let segment = file.getSegments()[anchors[0].segmentIndex];
segment.replace(start, end, response);
});
return true; return true;
} }

View File

@@ -1,6 +1,6 @@
import Dexie, { liveQuery } from 'dexie'; import Dexie, { liveQuery } from 'dexie';
import { GPXFile, GPXStatistics } from 'gpx'; 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 { writable, get, derived, type Readable, type Writable } from 'svelte/store';
import { fileOrder, initTargetMapBounds, selectedFiles, updateTargetMapBounds } from './stores'; import { fileOrder, initTargetMapBounds, selectedFiles, updateTargetMapBounds } from './stores';
import { mode } from 'mode-watcher'; import { mode } from 'mode-watcher';
@@ -218,12 +218,12 @@ function applyGlobal(callback: (files: Map<string, GPXFile>) => void) {
} }
// Helper function to apply a callback to multiple files // 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>) => GPXFile) {
const [newFileState, patch, inversePatch] = produceWithPatches(fileState, (draft) => { const [newFileState, patch, inversePatch] = produceWithPatches(fileState, (draft) => {
fileIds.forEach((fileId) => { fileIds.forEach((fileId) => {
let file = draft.get(fileId); let file = draft.get(fileId);
if (file) { 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>) => GPXFile) => {
applyToFiles([id], callback); applyToFiles([id], callback);
}, },
applyToSelectedFiles: (callback: (file: GPXFile) => void) => { applyToSelectedFiles: (callback: (file: WritableDraft<GPXFile>) => GPXFile) => {
applyToFiles(get(fileOrder).filter(fileId => get(selectedFiles).has(fileId)), callback); applyToFiles(get(fileOrder).filter(fileId => get(selectedFiles).has(fileId)), callback);
}, },
duplicateSelectedFiles: () => { duplicateSelectedFiles: () => {

View File

@@ -203,7 +203,7 @@ export function exportSelectedFiles() {
if (get(selectedFiles).has(fileId)) { if (get(selectedFiles).has(fileId)) {
let f = get(file); let f = get(file);
if (f) { if (f) {
exportFile(f); exportFile(f.file);
await new Promise(resolve => setTimeout(resolve, 200)); await new Promise(resolve => setTimeout(resolve, 200));
} }
} }
@@ -214,7 +214,7 @@ export function exportAllFiles() {
get(fileObservers).forEach(async (file) => { get(fileObservers).forEach(async (file) => {
let f = get(file); let f = get(file);
if (f) { if (f) {
exportFile(f); exportFile(f.file);
await new Promise(resolve => setTimeout(resolve, 200)); await new Promise(resolve => setTimeout(resolve, 200));
} }
}); });