diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts
index 90cdcd17..12227f0f 100644
--- a/gpx/src/gpx.ts
+++ b/gpx/src/gpx.ts
@@ -427,6 +427,14 @@ export class TrackPoint {
return this.attributes;
}
+ getLatitude(): number {
+ return this.attributes.lat;
+ }
+
+ getLongitude(): number {
+ return this.attributes.lon;
+ }
+
getHeartRate(): number {
return this.extensions && this.extensions['gpxtpx:TrackPointExtension'] && this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:hr'] ? this.extensions['gpxtpx:TrackPointExtension']['gpxtpx:hr'] : undefined;
}
diff --git a/gpx/src/index.ts b/gpx/src/index.ts
index 6fccb04e..a5e98cdb 100644
--- a/gpx/src/index.ts
+++ b/gpx/src/index.ts
@@ -1,4 +1,5 @@
export * from './gpx';
+export { Coordinates } from './types';
export { parseGPX, buildGPX } from './io';
diff --git a/website/src/lib/components/toolbar/Toolbar.svelte b/website/src/lib/components/toolbar/Toolbar.svelte
index dc33b3a1..0eb45f5a 100644
--- a/website/src/lib/components/toolbar/Toolbar.svelte
+++ b/website/src/lib/components/toolbar/Toolbar.svelte
@@ -1,6 +1,6 @@
@@ -23,11 +31,7 @@
-
{
- currentTool = currentTool === 'routing' ? null : 'routing';
- }}
- >
+
Edit the track points
diff --git a/website/src/lib/components/toolbar/Routing.svelte b/website/src/lib/components/toolbar/routing/Routing.svelte
similarity index 89%
rename from website/src/lib/components/toolbar/Routing.svelte
rename to website/src/lib/components/toolbar/routing/Routing.svelte
index 6e0667cb..bb178940 100644
--- a/website/src/lib/components/toolbar/Routing.svelte
+++ b/website/src/lib/components/toolbar/routing/Routing.svelte
@@ -1,14 +1,15 @@
diff --git a/website/src/lib/components/toolbar/routing/routing.ts b/website/src/lib/components/toolbar/routing/routing.ts
new file mode 100644
index 00000000..9d0744fa
--- /dev/null
+++ b/website/src/lib/components/toolbar/routing/routing.ts
@@ -0,0 +1,185 @@
+import type { Coordinates, GPXFile, TrackPoint } from "gpx";
+import mapboxgl from "mapbox-gl";
+
+export type TrackPointWithIndex = { point: TrackPoint, index: number };
+
+export class AnchorPointHierarchy {
+ level: number;
+ point: TrackPointWithIndex | null;
+ left: AnchorPointHierarchy[] | null;
+ right: AnchorPointHierarchy[] | null;
+ leftParent: AnchorPointHierarchy | null = null;
+ rightParent: AnchorPointHierarchy | null = null;
+
+ constructor(level: number, point: TrackPointWithIndex | null, left: AnchorPointHierarchy[] | null = null, right: AnchorPointHierarchy[] | null = null) {
+ this.level = level;
+ this.point = point;
+ this.left = left;
+ this.right = right;
+ }
+
+ setLeftParent(parent: AnchorPointHierarchy) {
+ this.leftParent = parent;
+ }
+
+ setRightParent(parent: AnchorPointHierarchy) {
+ this.rightParent = parent;
+ }
+
+ createMarkers(map: mapboxgl.Map, last: boolean = true) {
+ if (this.left == null && this.right == null && this.point) {
+ let marker = new mapboxgl.Marker({
+ draggable: true,
+ }).setLngLat([this.point.point.getLongitude(), this.point.point.getLatitude()]).addTo(map);
+ }
+
+ if (this.right) {
+ this.right.forEach((point, index) => {
+ if ((index < this.right.length - 1) || last) {
+ point.createMarkers(map, (index >= this.right.length - 2) && last);
+ }
+ });
+ }
+ }
+
+ static create(file: GPXFile, initialEpsilon: number = 1000, minEpsilon: number = 50): AnchorPointHierarchy {
+ let hierarchies = [];
+ for (let track of file.getChildren()) {
+ for (let segment of track.getChildren()) {
+ let points = segment.trkpt;
+ let hierarchy = new AnchorPointHierarchy(0, null);
+ hierarchy.right = AnchorPointHierarchy.createRecursive(1, points, initialEpsilon, minEpsilon);
+ hierarchies.push(hierarchy);
+ }
+ }
+ let hierarchy = new AnchorPointHierarchy(0, null);
+ hierarchy.right = hierarchies;
+ return hierarchy;
+ }
+
+ static createRecursive(level: number, points: TrackPoint[], epsilon: number, minEpsilon: number, start: number = 0, end: number = points.length - 1): AnchorPointHierarchy[] {
+ if (start == end) {
+ return [new AnchorPointHierarchy(level, { point: points[start], index: start })];
+ } else if (epsilon < minEpsilon || end - start == 1) {
+ return [new AnchorPointHierarchy(level, { point: points[start], index: start }), new AnchorPointHierarchy(level, { point: points[end], index: end })];
+ }
+
+ let simplified = ramerDouglasPeucker(points, epsilon, start, end);
+
+ let childHierarchies = [];
+
+ for (let i = 0; i < simplified.length - 1; i++) {
+ childHierarchies.push(AnchorPointHierarchy.createRecursive(level + 1, points, epsilon / 2, minEpsilon, simplified[i].index, simplified[i + 1].index));
+ }
+
+ let hierarchy = [];
+ for (let i = 0; i < simplified.length; i++) {
+ hierarchy.push(new AnchorPointHierarchy(
+ level,
+ simplified[i],
+ i > 0 ? childHierarchies[i - 1] : null,
+ i < simplified.length - 1 ? childHierarchies[i] : null
+ ));
+ if (i > 0 && childHierarchies[i - 1].length > 0) {
+ childHierarchies[i - 1][childHierarchies[i - 1].length - 1].setRightParent(hierarchy[i]);
+ }
+ if (i < simplified.length - 1 && childHierarchies[i].length > 0) {
+ childHierarchies[i][0].setLeftParent(hierarchy[i]);
+ }
+ }
+
+ return hierarchy;
+ }
+}
+
+function ramerDouglasPeucker(points: TrackPoint[], epsilon: number, start: number = 0, end: number = points.length - 1): TrackPointWithIndex[] {
+ let simplified = [{
+ point: points[start],
+ index: start
+ }];
+ ramerDouglasPeuckerRecursive(points, epsilon, start, end, simplified);
+ simplified.push({
+ point: points[end],
+ index: end
+ });
+ return simplified;
+}
+
+function ramerDouglasPeuckerRecursive(points: TrackPoint[], epsilon: number, start: number, end: number, simplified: TrackPointWithIndex[]) {
+ let largest = {
+ index: 0,
+ distance: 0
+ };
+
+ for (let i = start + 1; i < end; i++) {
+ let distance = crossarc(points[start].getCoordinates(), points[end].getCoordinates(), points[i].getCoordinates());
+ if (distance > largest.distance) {
+ largest.index = i;
+ largest.distance = distance;
+ }
+ }
+
+ if (largest.distance > epsilon) {
+ ramerDouglasPeuckerRecursive(points, epsilon, start, largest.index, simplified);
+ simplified.push({ point: points[largest.index], index: largest.index });
+ ramerDouglasPeuckerRecursive(points, epsilon, largest.index, end, simplified);
+ }
+}
+
+
+const earthRadius = 6371008.8;
+
+function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
+ // Calculates the shortest distance in meters
+ // between an arc (defined by p1 and p2) and a third point, p3.
+ // Input lat1,lon1,lat2,lon2,lat3,lon3 in degrees.
+
+ const rad = Math.PI / 180;
+ const lat1 = coord1.lat * rad;
+ const lat2 = coord2.lat * rad;
+ const lat3 = coord3.lat * rad;
+
+ const lon1 = coord1.lon * rad;
+ const lon2 = coord2.lon * rad;
+ const lon3 = coord3.lon * rad;
+
+ // Prerequisites for the formulas
+ const bear12 = bearing(lat1, lon1, lat2, lon2);
+ const bear13 = bearing(lat1, lon1, lat3, lon3);
+ let dis13 = distance(lat1, lon1, lat3, lon3);
+
+ let diff = Math.abs(bear13 - bear12);
+ if (diff > Math.PI) {
+ diff = 2 * Math.PI - diff;
+ }
+
+ // Is relative bearing obtuse?
+ if (diff > (Math.PI / 2)) {
+ return dis13;
+ }
+
+ // Find the cross-track distance.
+ let dxt = Math.asin(Math.sin(dis13 / earthRadius) * Math.sin(bear13 - bear12)) * earthRadius;
+
+ // Is p4 beyond the arc?
+ let dis12 = distance(lat1, lon1, lat2, lon2);
+ let dis14 = Math.acos(Math.cos(dis13 / earthRadius) / Math.cos(dxt / earthRadius)) * earthRadius;
+ if (dis14 > dis12) {
+ return distance(lat2, lon2, lat3, lon3);
+ } else {
+ return Math.abs(dxt);
+ }
+}
+
+function distance(latA: number, lonA: number, latB: number, lonB: number): number {
+ // Finds the distance between two lat / lon points.
+ return Math.acos(Math.sin(latA) * Math.sin(latB) + Math.cos(latA) * Math.cos(latB) * Math.cos(lonB - lonA)) * earthRadius;
+}
+
+
+function bearing(latA: number, lonA: number, latB: number, lonB: number): number {
+ // Finds the bearing from one lat / lon point to another.
+ return Math.atan2(Math.sin(lonB - lonA) * Math.cos(latB),
+ Math.cos(latA) * Math.sin(latB) - Math.sin(latA) * Math.cos(latB) * Math.cos(lonB - lonA));
+}
+