Add multi-day data model, computeDays, and GPX roundtrip

- overnight.ts: Yjs helpers to set/clear/check overnight flag on waypoints
- compute-days.ts: Pure function splitting routes into DayStage[] at day breaks
- use-days.ts: Reactive hook mapping Yjs state into computeDays() input
- GPX generate: emit <type>overnight</type> for isDayBreak waypoints
- GPX parse: recognize <type>overnight</type> and set isDayBreak on waypoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-10 23:51:31 +02:00
parent c9d6f65616
commit efdb3c1973
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
7 changed files with 226 additions and 6 deletions

View file

@ -0,0 +1,24 @@
import * as Y from "yjs";
import type { YjsState } from "./use-yjs.ts";
/**
* Set or clear the overnight flag on a waypoint.
*/
export function setOvernight(yjs: YjsState, index: number, value: boolean): void {
const waypointMap = yjs.waypoints.get(index);
if (!waypointMap) return;
yjs.doc.transact(() => {
if (value) {
waypointMap.set("overnight", true);
} else {
waypointMap.delete("overnight");
}
}, "local");
}
/**
* Check if a waypoint Y.Map has the overnight flag set.
*/
export function isOvernight(yMap: Y.Map<unknown>): boolean {
return yMap.get("overnight") === true;
}

View file

@ -0,0 +1,68 @@
import { useEffect, useState } from "react";
import * as Y from "yjs";
import { computeDays, type DayStage } from "@trails-cool/gpx";
import type { Waypoint } from "@trails-cool/types";
import type { TrackPoint } from "@trails-cool/gpx";
import type { YjsState } from "./use-yjs.ts";
import { isOvernight } from "./overnight.ts";
/**
* Reactive hook that computes day stages from Yjs waypoints and route data.
* Returns an empty array for single-day routes (no overnight waypoints).
*/
export function useDays(yjs: YjsState | null): DayStage[] {
const [days, setDays] = useState<DayStage[]>([]);
useEffect(() => {
if (!yjs) return;
const recompute = () => {
// Extract waypoints with isDayBreak from Yjs
const waypoints: Waypoint[] = yjs.waypoints.toArray().map((yMap: Y.Map<unknown>) => ({
lat: yMap.get("lat") as number,
lon: yMap.get("lon") as number,
name: yMap.get("name") as string | undefined,
isDayBreak: isOvernight(yMap) || undefined,
}));
// Check if any waypoint has isDayBreak
const hasBreaks = waypoints.some((w) => w.isDayBreak);
if (!hasBreaks) {
setDays([]);
return;
}
// Extract track points from route coordinates stored in Yjs
const coordsStr = yjs.routeData.get("coordinates") as string | undefined;
if (!coordsStr) {
setDays([]);
return;
}
try {
// coordinates are stored as [[lon, lat, ele], ...] (GeoJSON format)
const coords: number[][] = JSON.parse(coordsStr);
const trackPoints: TrackPoint[] = coords.map((c) => ({
lat: c[1]!,
lon: c[0]!,
ele: c[2],
}));
setDays(computeDays(waypoints, [trackPoints]));
} catch {
setDays([]);
}
};
yjs.waypoints.observeDeep(recompute);
yjs.routeData.observe(recompute);
recompute();
return () => {
yjs.waypoints.unobserveDeep(recompute);
yjs.routeData.unobserve(recompute);
};
}, [yjs]);
return days;
}