Files
gpx.studio/gpx/src/simplify.ts

163 lines
5.1 KiB
TypeScript
Raw Normal View History

import { TrackPoint } from './gpx';
import { Coordinates } from './types';
2024-06-11 19:08:46 +02:00
export type SimplifiedTrackPoint = { point: TrackPoint; distance?: number };
2024-06-11 19:08:46 +02: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) {
return [
{
point: points[0],
},
];
2024-06-11 19:08:46 +02: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({
point: points[points.length - 1],
2024-06-11 19:08:46 +02:00
});
return simplified;
}
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,
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
}
}
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
}
const metersPerLatitudeDegree = 111320;
2024-06-11 19:08:46 +02:00
function getMetersPerLongitudeDegree(latitude: number): number {
return Math.cos((latitude * Math.PI) / 180) * metersPerLatitudeDegree;
}
2024-06-11 19:08:46 +02: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
}
// 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
// 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));
}
export function projectedPoint(
point1: TrackPoint,
point2: TrackPoint,
point3: TrackPoint | Coordinates
): Coordinates {
return projected(
point1.getCoordinates(),
point2.getCoordinates(),
point3 instanceof TrackPoint ? point3.getCoordinates() : point3
);
}
function projected(coord1: Coordinates, coord2: Coordinates, coord3: Coordinates): Coordinates {
// Calculates the point on the line segment defined by p1 and p2
// that is closest to the 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 coord1;
}
// 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,
};
}