2024-04-23 14:11:05 +02:00
|
|
|
import type { Coordinates, GPXFile, TrackPoint } from "gpx";
|
|
|
|
import mapboxgl from "mapbox-gl";
|
|
|
|
|
|
|
|
export type TrackPointWithIndex = { point: TrackPoint, index: number };
|
|
|
|
|
|
|
|
export class AnchorPointHierarchy {
|
|
|
|
level: number;
|
2024-04-23 14:39:55 +02:00
|
|
|
lowestLevel: number;
|
2024-04-23 14:11:05 +02:00
|
|
|
point: TrackPointWithIndex | null;
|
2024-04-23 14:39:55 +02:00
|
|
|
left: AnchorPointHierarchy[] | null = null;
|
|
|
|
right: AnchorPointHierarchy[] | null = null;
|
2024-04-23 14:11:05 +02:00
|
|
|
leftParent: AnchorPointHierarchy | null = null;
|
|
|
|
rightParent: AnchorPointHierarchy | null = null;
|
|
|
|
|
2024-04-23 14:39:55 +02:00
|
|
|
constructor(level: number, point: TrackPointWithIndex | null) {
|
2024-04-23 14:11:05 +02:00
|
|
|
this.level = level;
|
2024-04-23 14:39:55 +02:00
|
|
|
this.lowestLevel = level;
|
2024-04-23 14:11:05 +02:00
|
|
|
this.point = point;
|
|
|
|
}
|
|
|
|
|
2024-04-23 16:20:47 +02:00
|
|
|
getMarkers(map: mapboxgl.Map, last: boolean = true, markers: mapboxgl.Marker[] = []): mapboxgl.Marker[] {
|
2024-04-23 14:11:05 +02:00
|
|
|
if (this.left == null && this.right == null && this.point) {
|
2024-04-23 16:20:47 +02:00
|
|
|
let element = document.createElement('div');
|
|
|
|
element.className = 'hidden h-3 w-3 rounded-full bg-background border-2 border-black';
|
2024-04-23 14:11:05 +02:00
|
|
|
let marker = new mapboxgl.Marker({
|
|
|
|
draggable: true,
|
2024-04-23 16:20:47 +02:00
|
|
|
element
|
|
|
|
});
|
|
|
|
marker.setLngLat(this.point.point.getCoordinates());
|
|
|
|
marker.addTo(map);
|
|
|
|
Object.defineProperty(marker, '_hierarchy', { value: this });
|
|
|
|
markers.push(marker);
|
2024-04-23 14:11:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.right) {
|
|
|
|
this.right.forEach((point, index) => {
|
|
|
|
if ((index < this.right.length - 1) || last) {
|
2024-04-23 16:20:47 +02:00
|
|
|
// (index >= this.right.length - 2) because the last point must be drawn by the second to last AnchorPointHierarchy
|
|
|
|
// because only the right children are drawn
|
|
|
|
point.getMarkers(map, (index >= this.right.length - 2) && last, markers);
|
2024-04-23 14:11:05 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2024-04-23 16:20:47 +02:00
|
|
|
|
|
|
|
return markers;
|
2024-04-23 14:11:05 +02:00
|
|
|
}
|
|
|
|
|
2024-04-23 16:20:47 +02:00
|
|
|
static create(file: GPXFile, initialEpsilon: number = 50000, minEpsilon: number = 50): AnchorPointHierarchy {
|
2024-04-23 14:11:05 +02:00
|
|
|
let hierarchies = [];
|
|
|
|
for (let track of file.getChildren()) {
|
|
|
|
for (let segment of track.getChildren()) {
|
|
|
|
let points = segment.trkpt;
|
|
|
|
let hierarchy = new AnchorPointHierarchy(0, null);
|
2024-04-23 14:39:55 +02:00
|
|
|
hierarchy.right = AnchorPointHierarchy.createRecursive(1, 1, 1, points, initialEpsilon, minEpsilon);
|
2024-04-23 14:11:05 +02:00
|
|
|
hierarchies.push(hierarchy);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let hierarchy = new AnchorPointHierarchy(0, null);
|
|
|
|
hierarchy.right = hierarchies;
|
|
|
|
return hierarchy;
|
|
|
|
}
|
|
|
|
|
2024-04-23 14:39:55 +02:00
|
|
|
static createRecursive(level: number, levelLeft: number, levelRight: number, points: TrackPoint[], epsilon: number, minEpsilon: number, start: number = 0, end: number = points.length - 1): AnchorPointHierarchy[] {
|
2024-04-23 14:11:05 +02:00
|
|
|
if (start == end) {
|
2024-04-23 14:39:55 +02:00
|
|
|
return [new AnchorPointHierarchy(Math.min(levelLeft, levelRight), { point: points[start], index: start })];
|
2024-04-23 14:11:05 +02:00
|
|
|
} else if (epsilon < minEpsilon || end - start == 1) {
|
2024-04-23 14:39:55 +02:00
|
|
|
return [new AnchorPointHierarchy(levelLeft, { point: points[start], index: start }), new AnchorPointHierarchy(levelRight, { point: points[end], index: end })];
|
2024-04-23 14:11:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
let simplified = ramerDouglasPeucker(points, epsilon, start, end);
|
|
|
|
|
|
|
|
let hierarchy = [];
|
|
|
|
for (let i = 0; i < simplified.length; i++) {
|
|
|
|
hierarchy.push(new AnchorPointHierarchy(
|
2024-04-23 14:39:55 +02:00
|
|
|
i == 0 ? levelLeft : i == simplified.length - 1 ? levelRight : level,
|
|
|
|
simplified[i]
|
2024-04-23 14:11:05 +02:00
|
|
|
));
|
2024-04-23 14:39:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
let childHierarchies = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < simplified.length - 1; i++) {
|
2024-04-23 16:20:47 +02:00
|
|
|
childHierarchies.push(AnchorPointHierarchy.createRecursive(level + 1, i == 0 ? levelLeft : level, i == simplified.length - 2 ? levelRight : level, points, epsilon / 1.54, minEpsilon, simplified[i].index, simplified[i + 1].index));
|
2024-04-23 14:39:55 +02:00
|
|
|
|
|
|
|
hierarchy[i].right = childHierarchies[i];
|
|
|
|
hierarchy[i + 1].left = childHierarchies[i];
|
2024-04-23 14:11:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
|