trails/packages/gpx/src/waypoints.ts
Ullrich Schäfer 857100ef94
Fix overnight flag lost on Journal roundtrip
Three gaps in the Planner → Journal → Planner roundtrip:

1. SaveToJournalButton: wasn't including isDayBreak when building
   waypoints for GPX generation
2. PlannerMap GPX import: wasn't setting overnight on Y.Maps when
   importing waypoints with isDayBreak
3. use-yjs initial waypoints: wasn't setting overnight from isDayBreak

Also widened the waypoint type throughout the chain (api.sessions,
session.$id, SessionView, extractWaypoints) to carry isDayBreak.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:27:09 +02:00

97 lines
3.3 KiB
TypeScript

import type { GpxData } from "./types.ts";
/**
* Extract waypoints from parsed GPX data.
* Uses explicit <wpt> 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; isDayBreak?: boolean }> {
if (gpxData.waypoints.length > 0) return gpxData.waypoints;
if (gpxData.tracks.length === 0) return [];
// 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 = segmentEndpoints[segmentEndpoints.length - 1];
if (!prev || prev.lat !== first.lat || prev.lon !== first.lon) {
segmentEndpoints.push({ lat: first.lat, lon: first.lon });
}
}
const lastSeg = gpxData.tracks[gpxData.tracks.length - 1]!;
if (lastSeg.length > 0) {
const last = lastSeg[lastSeg.length - 1]!;
const prev = segmentEndpoints[segmentEndpoints.length - 1];
if (!prev || prev.lat !== last.lat || prev.lon !== last.lon) {
segmentEndpoints.push({ lat: last.lat, lon: last.lon });
}
}
// 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.005);
}
/**
* 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);
}