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>
97 lines
3.3 KiB
TypeScript
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);
|
|
}
|