2025-02-02 11:17:22 +01:00
|
|
|
import { TrackPoint } from './gpx';
|
|
|
|
|
import { Coordinates } from './types';
|
2024-06-11 19:08:46 +02:00
|
|
|
|
2025-02-02 11:17:22 +01:00
|
|
|
export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
|
2024-06-11 19:08:46 +02:00
|
|
|
|
2025-02-02 11:17:22 +01:00
|
|
|
export function ramerDouglasPeucker(
|
|
|
|
|
points: TrackPoint[],
|
|
|
|
|
epsilon: number = 50,
|
|
|
|
|
measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number = crossarcDistance
|
|
|
|
|
): SimplifiedTrackPoint[] {
|
2024-06-11 19:08:46 +02:00
|
|
|
if (points.length == 0) {
|
|
|
|
|
return [];
|
|
|
|
|
} else if (points.length == 1) {
|
2025-02-02 11:17:22 +01:00
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
point: points[0],
|
|
|
|
|
},
|
|
|
|
|
];
|
2024-06-11 19:08:46 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-02 11:17:22 +01:00
|
|
|
let simplified = [
|
|
|
|
|
{
|
|
|
|
|
point: points[0],
|
|
|
|
|
},
|
|
|
|
|
];
|
2024-06-28 18:40:43 +02:00
|
|
|
ramerDouglasPeuckerRecursive(points, epsilon, measure, 0, points.length - 1, simplified);
|
2024-06-11 19:08:46 +02:00
|
|
|
simplified.push({
|
2025-02-02 11:17:22 +01:00
|
|
|
point: points[points.length - 1],
|
2024-06-11 19:08:46 +02:00
|
|
|
});
|
|
|
|
|
return simplified;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-02 11:17:22 +01:00
|
|
|
function ramerDouglasPeuckerRecursive(
|
|
|
|
|
points: TrackPoint[],
|
|
|
|
|
epsilon: number,
|
|
|
|
|
measure: (a: TrackPoint, b: TrackPoint, c: TrackPoint) => number,
|
|
|
|
|
start: number,
|
|
|
|
|
end: number,
|
|
|
|
|
simplified: SimplifiedTrackPoint[]
|
|
|
|
|
) {
|
2024-06-11 19:08:46 +02:00
|
|
|
let largest = {
|
|
|
|
|
index: 0,
|
2025-02-02 11:17:22 +01:00
|
|
|
distance: 0,
|
2024-06-11 19:08:46 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (let i = start + 1; i < end; i++) {
|
2024-06-28 18:40:43 +02:00
|
|
|
let distance = measure(points[start], points[end], points[i]);
|
|
|
|
|
if (distance > largest.distance) {
|
|
|
|
|
largest.index = i;
|
|
|
|
|
largest.distance = distance;
|
2024-06-11 19:08:46 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (largest.distance > epsilon && largest.index != 0) {
|
2024-06-28 18:40:43 +02:00
|
|
|
ramerDouglasPeuckerRecursive(points, epsilon, measure, start, largest.index, simplified);
|
2024-06-11 19:08:46 +02:00
|
|
|
simplified.push({ point: points[largest.index], distance: largest.distance });
|
2024-06-28 18:40:43 +02:00
|
|
|
ramerDouglasPeuckerRecursive(points, epsilon, measure, largest.index, end, simplified);
|
2024-06-11 19:08:46 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-02 11:17:22 +01:00
|
|
|
export function crossarcDistance(
|
|
|
|
|
point1: TrackPoint,
|
|
|
|
|
point2: TrackPoint,
|
|
|
|
|
point3: TrackPoint | Coordinates
|
|
|
|
|
): number {
|
|
|
|
|
return crossarc(
|
|
|
|
|
point1.getCoordinates(),
|
|
|
|
|
point2.getCoordinates(),
|
|
|
|
|
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
|
|
|
|
|
);
|
2024-06-25 19:41:37 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-15 07:17:11 +01:00
|
|
|
const metersPerLatitudeDegree = 111320;
|
2024-06-11 19:08:46 +02:00
|
|
|
|
2025-11-15 07:17:11 +01:00
|
|
|
function getMetersPerLongitudeDegree(latitude: number): number {
|
|
|
|
|
return Math.cos((latitude * Math.PI) / 180) * metersPerLatitudeDegree;
|
|
|
|
|
}
|
2024-06-11 19:08:46 +02:00
|
|
|
|
2025-11-15 07:17:11 +01:00
|
|
|
function crossarc(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): number {
|
|
|
|
|
// Calculates the perpendicular distance in meters
|
|
|
|
|
// between a line segment (defined by p1 and p2) and a third point, p3.
|
|
|
|
|
// Uses simple planar geometry (ignores earth curvature).
|
|
|
|
|
|
|
|
|
|
// Convert to meters using approximate scaling
|
|
|
|
|
const metersPerLongitudeDegree = getMetersPerLongitudeDegree(coord1.lat);
|
|
|
|
|
|
|
|
|
|
const x1 = coord1.lon * metersPerLongitudeDegree;
|
|
|
|
|
const y1 = coord1.lat * metersPerLatitudeDegree;
|
|
|
|
|
const x2 = coord2.lon * metersPerLongitudeDegree;
|
|
|
|
|
const y2 = coord2.lat * metersPerLatitudeDegree;
|
|
|
|
|
const x3 = coord3.lon * metersPerLongitudeDegree;
|
|
|
|
|
const y3 = coord3.lat * metersPerLatitudeDegree;
|
|
|
|
|
|
|
|
|
|
const dx = x2 - x1;
|
|
|
|
|
const dy = y2 - y1;
|
|
|
|
|
const segmentLengthSquared = dx * dx + dy * dy;
|
|
|
|
|
|
|
|
|
|
if (segmentLengthSquared === 0) {
|
|
|
|
|
// p1 and p2 are the same point
|
|
|
|
|
return Math.sqrt((x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1));
|
2024-06-11 19:08:46 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-15 07:17:11 +01:00
|
|
|
// Project p3 onto the line defined by p1-p2
|
|
|
|
|
const t = Math.max(0, Math.min(1, ((x3 - x1) * dx + (y3 - y1) * dy) / segmentLengthSquared));
|
2024-06-11 19:08:46 +02:00
|
|
|
|
2025-11-15 07:17:11 +01:00
|
|
|
// Find the closest point on the segment
|
|
|
|
|
const projX = x1 + t * dx;
|
|
|
|
|
const projY = y1 + t * dy;
|
|
|
|
|
|
|
|
|
|
// Return distance from p3 to the projected point
|
|
|
|
|
return Math.sqrt((x3 - projX) * (x3 - projX) + (y3 - projY) * (y3 - projY));
|
2024-09-04 14:42:26 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-02 11:17:22 +01:00
|
|
|
export function projectedPoint(
|
|
|
|
|
point1: TrackPoint,
|
|
|
|
|
point2: TrackPoint,
|
|
|
|
|
point3: TrackPoint | Coordinates
|
|
|
|
|
): Coordinates {
|
|
|
|
|
return projected(
|
|
|
|
|
point1.getCoordinates(),
|
|
|
|
|
point2.getCoordinates(),
|
|
|
|
|
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
|
|
|
|
|
);
|
2024-09-04 14:42:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
|
2025-11-15 07:17:11 +01:00
|
|
|
// Calculates the point on the line segment defined by p1 and p2
|
2024-09-04 14:42:26 +02:00
|
|
|
// that is closest to the third point, p3.
|
2025-11-15 07:17:11 +01:00
|
|
|
// Uses simple planar geometry (ignores earth curvature).
|
2024-09-04 14:42:26 +02:00
|
|
|
|
2025-11-15 07:17:11 +01:00
|
|
|
// Convert to meters using approximate scaling
|
|
|
|
|
const metersPerLongitudeDegree = getMetersPerLongitudeDegree(coord1.lat);
|
2024-09-04 14:42:26 +02:00
|
|
|
|
2025-11-15 07:17:11 +01:00
|
|
|
const x1 = coord1.lon * metersPerLongitudeDegree;
|
|
|
|
|
const y1 = coord1.lat * metersPerLatitudeDegree;
|
|
|
|
|
const x2 = coord2.lon * metersPerLongitudeDegree;
|
|
|
|
|
const y2 = coord2.lat * metersPerLatitudeDegree;
|
|
|
|
|
const x3 = coord3.lon * metersPerLongitudeDegree;
|
|
|
|
|
const y3 = coord3.lat * metersPerLatitudeDegree;
|
2024-09-04 14:42:26 +02:00
|
|
|
|
2025-11-15 07:17:11 +01:00
|
|
|
const dx = x2 - x1;
|
|
|
|
|
const dy = y2 - y1;
|
|
|
|
|
const segmentLengthSquared = dx * dx + dy * dy;
|
2024-09-04 14:42:26 +02:00
|
|
|
|
2025-11-15 07:17:11 +01:00
|
|
|
if (segmentLengthSquared === 0) {
|
|
|
|
|
// p1 and p2 are the same point
|
2024-09-04 14:42:26 +02:00
|
|
|
return coord1;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-15 07:17:11 +01:00
|
|
|
// Project p3 onto the line defined by p1-p2
|
|
|
|
|
const t = Math.max(0, Math.min(1, ((x3 - x1) * dx + (y3 - y1) * dy) / segmentLengthSquared));
|
|
|
|
|
|
|
|
|
|
// Find the closest point on the segment
|
|
|
|
|
const projX = x1 + t * dx;
|
|
|
|
|
const projY = y1 + t * dy;
|
|
|
|
|
|
|
|
|
|
// Convert back to degrees
|
|
|
|
|
return {
|
|
|
|
|
lat: projY / metersPerLatitudeDegree,
|
|
|
|
|
lon: projX / metersPerLongitudeDegree,
|
|
|
|
|
};
|
2025-02-02 11:17:22 +01:00
|
|
|
}
|