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:
parent
c9d6f65616
commit
efdb3c1973
7 changed files with 226 additions and 6 deletions
24
apps/planner/app/lib/overnight.ts
Normal file
24
apps/planner/app/lib/overnight.ts
Normal 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;
|
||||
}
|
||||
68
apps/planner/app/lib/use-days.ts
Normal file
68
apps/planner/app/lib/use-days.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue