From 0e27d6eaef26d2630785622cbbfad1573396aedc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Thu, 2 Apr 2026 22:36:13 +0100 Subject: [PATCH] Use Douglas-Peucker simplification for single-segment GPX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For GPX files with one long track segment, extracting only start/end gives just 2 waypoints. Now uses Douglas-Peucker line simplification (epsilon=0.05° ≈ 5km) to find significant turning points. Result: berlin-dresden-radweg (249km, 5660 points) → 10 waypoints that capture the route's key direction changes. Multi-segment GPX files still use segment endpoints as before. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/gpx/src/waypoints.ts | 84 ++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 10 deletions(-) diff --git a/packages/gpx/src/waypoints.ts b/packages/gpx/src/waypoints.ts index eca8da4..4d4da9b 100644 --- a/packages/gpx/src/waypoints.ts +++ b/packages/gpx/src/waypoints.ts @@ -2,32 +2,96 @@ import type { GpxData } from "./types.ts"; /** * Extract waypoints from parsed GPX data. - * Uses explicit elements if present, otherwise extracts the start - * of each track segment + end of the last segment. + * Uses explicit elements if present, otherwise simplifies the + * track using Douglas-Peucker to find significant turning points. */ export function extractWaypoints(gpxData: GpxData): Array<{ lat: number; lon: number; name?: string }> { if (gpxData.waypoints.length > 0) return gpxData.waypoints; if (gpxData.tracks.length === 0) return []; - const result: Array<{ lat: number; lon: number }> = []; + // Collect start of each segment + end of last (for multi-segment GPX) + const segmentEndpoints: Array<{ lat: number; lon: number }> = []; for (const seg of gpxData.tracks) { if (seg.length === 0) continue; const first = seg[0]!; - const prev = result[result.length - 1]; + const prev = segmentEndpoints[segmentEndpoints.length - 1]; if (!prev || prev.lat !== first.lat || prev.lon !== first.lon) { - result.push({ lat: first.lat, lon: first.lon }); + segmentEndpoints.push({ lat: first.lat, lon: first.lon }); } } - - // Add end of last segment const lastSeg = gpxData.tracks[gpxData.tracks.length - 1]!; if (lastSeg.length > 0) { const last = lastSeg[lastSeg.length - 1]!; - const prev = result[result.length - 1]; + const prev = segmentEndpoints[segmentEndpoints.length - 1]; if (!prev || prev.lat !== last.lat || prev.lon !== last.lon) { - result.push({ lat: last.lat, lon: last.lon }); + segmentEndpoints.push({ lat: last.lat, lon: last.lon }); } } - return result; + // If multi-segment already gives us enough waypoints, use those + if (segmentEndpoints.length > 2) return segmentEndpoints; + + // Single segment: simplify the track to find key turning points + const allPoints = gpxData.tracks.flat().map((p) => ({ lat: p.lat, lon: p.lon })); + if (allPoints.length < 2) return allPoints; + + return douglasPeucker(allPoints, 0.05); +} + +/** + * Douglas-Peucker line simplification. + * Epsilon is in degrees (~0.005° ≈ 500m at mid-latitudes). + * Recursively finds the point with maximum perpendicular distance + * from the line between start and end, keeping significant turns. + */ +function douglasPeucker( + points: Array<{ lat: number; lon: number }>, + epsilon: number, +): Array<{ lat: number; lon: number }> { + if (points.length <= 2) return points; + + let maxDist = 0; + let maxIdx = 0; + + const start = points[0]!; + const end = points[points.length - 1]!; + + for (let i = 1; i < points.length - 1; i++) { + const dist = perpendicularDistance(points[i]!, start, end); + if (dist > maxDist) { + maxDist = dist; + maxIdx = i; + } + } + + if (maxDist > epsilon) { + const left = douglasPeucker(points.slice(0, maxIdx + 1), epsilon); + const right = douglasPeucker(points.slice(maxIdx), epsilon); + return [...left.slice(0, -1), ...right]; + } + + return [start, end]; +} + +function perpendicularDistance( + point: { lat: number; lon: number }, + lineStart: { lat: number; lon: number }, + lineEnd: { lat: number; lon: number }, +): number { + const dx = lineEnd.lon - lineStart.lon; + const dy = lineEnd.lat - lineStart.lat; + const lenSq = dx * dx + dy * dy; + if (lenSq === 0) { + const px = point.lon - lineStart.lon; + const py = point.lat - lineStart.lat; + return Math.sqrt(px * px + py * py); + } + const t = Math.max(0, Math.min(1, + ((point.lon - lineStart.lon) * dx + (point.lat - lineStart.lat) * dy) / lenSq, + )); + const projLon = lineStart.lon + t * dx; + const projLat = lineStart.lat + t * dy; + const px = point.lon - projLon; + const py = point.lat - projLat; + return Math.sqrt(px * px + py * py); }