2024-05-23 12:57:24 +02:00
|
|
|
import type { Coordinates, GPXFile, TrackPoint, TrackSegment } from "gpx";
|
2024-04-25 16:41:06 +02:00
|
|
|
|
2024-04-30 15:19:50 +02:00
|
|
|
type SimplifiedTrackPoint = { point: TrackPoint, distance?: number };
|
2024-04-25 16:41:06 +02:00
|
|
|
|
|
|
|
const earthRadius = 6371008.8;
|
|
|
|
|
|
|
|
export function getZoomLevelForDistance(latitude: number, distance?: number): number {
|
|
|
|
if (distance === undefined) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
const rad = Math.PI / 180;
|
|
|
|
const lat = latitude * rad;
|
|
|
|
|
|
|
|
return Math.min(20, Math.max(0, Math.floor(Math.log2((earthRadius * Math.cos(lat)) / distance))));
|
|
|
|
}
|
|
|
|
|
2024-05-23 12:57:24 +02:00
|
|
|
export function updateAnchorPoints(file: GPXFile) {
|
|
|
|
let segments = file.getSegments();
|
|
|
|
|
2024-05-24 16:37:26 +02:00
|
|
|
for (let segment of segments) {
|
2024-05-23 12:57:24 +02:00
|
|
|
if (!segment._data.anchors) { // New segment, compute anchor points for it
|
|
|
|
computeAnchorPoints(segment);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (segment.trkpt.length > 0) {
|
|
|
|
if (!segment.trkpt[0]._data.anchor) { // First point is not an anchor, make it one
|
|
|
|
segment.trkpt[0]._data.anchor = true;
|
|
|
|
segment.trkpt[0]._data.zoom = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!segment.trkpt[segment.trkpt.length - 1]._data.anchor) { // Last point is not an anchor, make it one
|
|
|
|
segment.trkpt[segment.trkpt.length - 1]._data.anchor = true;
|
|
|
|
segment.trkpt[segment.trkpt.length - 1]._data.zoom = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function computeAnchorPoints(segment: TrackSegment) {
|
2024-04-25 19:02:34 +02:00
|
|
|
let points = segment.trkpt;
|
|
|
|
let anchors = ramerDouglasPeucker(points);
|
2024-04-30 15:19:50 +02:00
|
|
|
anchors.forEach((anchor) => {
|
|
|
|
let point = anchor.point;
|
|
|
|
point._data.anchor = true;
|
|
|
|
point._data.zoom = getZoomLevelForDistance(point.getLatitude(), anchor.distance);
|
2024-04-25 19:02:34 +02:00
|
|
|
});
|
2024-04-30 15:19:50 +02:00
|
|
|
segment._data.anchors = true;
|
2024-04-25 16:41:06 +02:00
|
|
|
}
|
|
|
|
|
2024-05-16 13:27:12 +02:00
|
|
|
export function ramerDouglasPeucker(points: readonly TrackPoint[], epsilon: number = 50, start: number = 0, end: number = points.length - 1): SimplifiedTrackPoint[] {
|
2024-04-27 12:18:40 +02:00
|
|
|
if (points.length == 0) {
|
|
|
|
return [];
|
|
|
|
} else if (points.length == 1) {
|
|
|
|
return [{
|
|
|
|
point: points[0]
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
2024-04-25 16:41:06 +02:00
|
|
|
let simplified = [{
|
2024-04-25 19:02:34 +02:00
|
|
|
point: points[start]
|
2024-04-25 16:41:06 +02:00
|
|
|
}];
|
|
|
|
ramerDouglasPeuckerRecursive(points, epsilon, start, end, simplified);
|
|
|
|
simplified.push({
|
2024-04-25 19:02:34 +02:00
|
|
|
point: points[end]
|
2024-04-25 16:41:06 +02:00
|
|
|
});
|
|
|
|
return simplified;
|
|
|
|
}
|
|
|
|
|
2024-05-16 13:27:12 +02:00
|
|
|
function ramerDouglasPeuckerRecursive(points: readonly TrackPoint[], epsilon: number, start: number, end: number, simplified: SimplifiedTrackPoint[]) {
|
2024-04-25 16:41:06 +02:00
|
|
|
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], distance: largest.distance });
|
|
|
|
ramerDouglasPeuckerRecursive(points, epsilon, largest.index, end, simplified);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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));
|
|
|
|
}
|