trails/apps/planner/app/lib/waypoint-ymap.ts
Ullrich Schäfer 3b9672e0ff
planner: give the Yjs document a typed schema seam
The routeData Y.Map's ~15 string keys (geojson, coordinates,
segmentBoundaries, road metadata, profile, colorMode, baseLayer,
overlays, poiCategories) were read and written raw at ~30 call sites,
each with its own JSON parsing and casts; parseJsonArray existed twice
and waypoint extraction four times. GPX assembly was duplicated between
SaveToJournalButton and ExportButton, so the saved plan and the
exported file could silently diverge.

- new lib/route-data.ts owns the routeData (+ noGoAreas) schema:
  typed read/write, JSON encoding internal, ColorMode moves here
  (re-exported from ColoredRoute for existing importers)
- new lib/gpx-export.ts owns GPX assembly: buildRouteGpx /
  buildPlanGpx / buildDayGpxFiles / hasDayBreaks; multi-day splitting
  becomes a pure, tested function
- waypoint-ymap.ts gains extractWaypoints / extractWaypointData; the
  four hand-rolled copies (use-routing, use-waypoint-manager,
  WaypointSidebar, use-days) now share it, and WaypointSidebar's
  moveWaypoint reuses the round-trip helpers instead of re-listing
  every waypoint field
- all hooks/components consume the seam; no raw routeData key strings
  remain outside route-data.ts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 01:35:04 +02:00

65 lines
2.1 KiB
TypeScript

import * as Y from "yjs";
import type { Waypoint, WaypointPoiTags } from "@trails-cool/types";
/**
* Reads all Waypoint fields from a Yjs map.
* The "overnight" key maps to isDayBreak (legacy wire name).
* Add new Waypoint fields here — one place for all consumers.
*/
export function waypointFromYMap(yMap: Y.Map<unknown>): Waypoint {
return {
lat: yMap.get("lat") as number,
lon: yMap.get("lon") as number,
name: yMap.get("name") as string | undefined,
note: yMap.get("note") as string | undefined,
isDayBreak: yMap.get("overnight") === true ? true : undefined,
osmId: yMap.get("osmId") as number | undefined,
poiTags: yMap.get("poiTags") as WaypointPoiTags | undefined,
};
}
/**
* Writes all Waypoint fields onto an existing Yjs map.
* The isDayBreak field is stored as "overnight" (legacy wire name).
* Add new Waypoint fields here — one place for all producers.
*/
export function applyWaypointToYMap(yMap: Y.Map<unknown>, wp: Waypoint): void {
yMap.set("lat", wp.lat);
yMap.set("lon", wp.lon);
if (wp.name) yMap.set("name", wp.name);
if (wp.note) yMap.set("note", wp.note);
if (wp.isDayBreak) yMap.set("overnight", true);
if (wp.osmId !== undefined) yMap.set("osmId", wp.osmId);
if (wp.poiTags) yMap.set("poiTags", wp.poiTags);
}
/** Convenience: creates a new Y.Map and populates it from a Waypoint. */
export function waypointToYMap(wp: Waypoint): Y.Map<unknown> {
const yMap = new Y.Map<unknown>();
applyWaypointToYMap(yMap, wp);
return yMap;
}
/** Reads the whole shared waypoint list as plain Waypoints. */
export function extractWaypoints(waypoints: Y.Array<Y.Map<unknown>>): Waypoint[] {
return waypoints.toArray().map(waypointFromYMap);
}
/** Waypoint flattened for UI state (isDayBreak as a plain boolean). */
export interface WaypointData {
lat: number;
lon: number;
name?: string;
note?: string;
overnight: boolean;
}
export function extractWaypointData(waypoints: Y.Array<Y.Map<unknown>>): WaypointData[] {
return extractWaypoints(waypoints).map((wp) => ({
lat: wp.lat,
lon: wp.lon,
name: wp.name,
note: wp.note,
overnight: wp.isDayBreak === true,
}));
}