diff --git a/apps/journal/app/routes/api.routes.$id.edit-in-planner.ts b/apps/journal/app/routes/api.routes.$id.edit-in-planner.ts index 6f4e502..a37f2fb 100644 --- a/apps/journal/app/routes/api.routes.$id.edit-in-planner.ts +++ b/apps/journal/app/routes/api.routes.$id.edit-in-planner.ts @@ -35,7 +35,7 @@ export async function action({ params, request }: Route.ActionArgs) { const session = (await sessionResp.json()) as { url: string; - initialWaypoints?: Array<{ lat: number; lon: number; name?: string }>; + initialWaypoints?: Array<{ lat: number; lon: number; name?: string; isDayBreak?: boolean; note?: string; osmId?: number; poiTags?: Record }>; initialNoGoAreas?: Array<{ points: Array<{ lat: number; lon: number }> }>; initialNotes?: string; }; diff --git a/apps/journal/app/routes/routes.$id.tsx b/apps/journal/app/routes/routes.$id.tsx index 8e44af4..27dc611 100644 --- a/apps/journal/app/routes/routes.$id.tsx +++ b/apps/journal/app/routes/routes.$id.tsx @@ -32,7 +32,7 @@ export async function loader({ params, request }: Route.LoaderArgs) { // Parse GPX once for day stats and waypoint POI data let dayStats: Array<{ dayNumber: number; startName?: string; endName?: string; distance: number; ascent: number; descent: number }> = []; - let waypoints: Array<{ lat: number; lon: number; name?: string; isDayBreak?: boolean; osmId?: number; poiTags?: Record }> = []; + let waypoints: Array<{ lat: number; lon: number; name?: string; isDayBreak?: boolean; note?: string; osmId?: number; poiTags?: Record }> = []; if (route.gpx) { try { const { computeDays, parseGpxAsync } = await import("@trails-cool/gpx"); @@ -42,6 +42,7 @@ export async function loader({ params, request }: Route.LoaderArgs) { lon: w.lon, name: w.name, isDayBreak: w.isDayBreak, + note: w.note, osmId: w.osmId, poiTags: w.poiTags as Record | undefined, })); @@ -398,15 +399,18 @@ export default function RouteDetailPage({ loaderData }: Route.ComponentProps) { )} - {waypoints.some((w) => w.osmId || w.poiTags) && ( + {waypoints.some((w) => w.osmId || w.poiTags || w.note) && (

{t("routes.waypoints")}

    - {waypoints.filter((w) => w.osmId || w.poiTags || w.name).map((w, i) => ( + {waypoints.filter((w) => w.osmId || w.poiTags || w.note || w.name).map((w, i) => (
  • {w.name ?? `${w.lat.toFixed(5)}, ${w.lon.toFixed(5)}`}

    + {w.note && ( +

    {w.note}

    + )} {w.poiTags && (
    {w.poiTags.phone && ( diff --git a/apps/planner/app/components/ExportButton.tsx b/apps/planner/app/components/ExportButton.tsx index ca93b23..31b99c2 100644 --- a/apps/planner/app/components/ExportButton.tsx +++ b/apps/planner/app/components/ExportButton.tsx @@ -4,7 +4,7 @@ import * as Y from "yjs"; import type { YjsState } from "~/lib/use-yjs"; import { generateGpx, computeDays } from "@trails-cool/gpx"; import type { TrackPoint, NoGoArea } from "@trails-cool/gpx"; -import type { WaypointPoiTags } from "@trails-cool/types"; +import { waypointFromYMap } from "~/lib/waypoint-ymap"; function getTracks(yjs: YjsState): TrackPoint[][] { const geojsonStr = yjs.routeData.get("geojson") as string | undefined; @@ -20,15 +20,7 @@ function getTracks(yjs: YjsState): TrackPoint[][] { } function getWaypoints(yjs: YjsState) { - return yjs.waypoints.toArray().map((yMap: Y.Map) => ({ - lat: yMap.get("lat") as number, - lon: yMap.get("lon") as number, - name: yMap.get("name") as string | undefined, - isDayBreak: yMap.get("overnight") === true ? true : undefined, - note: yMap.get("note") as string | undefined, - osmId: yMap.get("osmId") as number | undefined, - poiTags: yMap.get("poiTags") as WaypointPoiTags | undefined, - })); + return yjs.waypoints.toArray().map(waypointFromYMap); } function getNoGoAreas(yjs: YjsState): NoGoArea[] { diff --git a/apps/planner/app/components/SaveToJournalButton.tsx b/apps/planner/app/components/SaveToJournalButton.tsx index 2ba7885..60be654 100644 --- a/apps/planner/app/components/SaveToJournalButton.tsx +++ b/apps/planner/app/components/SaveToJournalButton.tsx @@ -4,7 +4,7 @@ import * as Y from "yjs"; import type { YjsState } from "~/lib/use-yjs"; import { generateGpx } from "@trails-cool/gpx"; import type { TrackPoint, NoGoArea } from "@trails-cool/gpx"; -import type { WaypointPoiTags } from "@trails-cool/types"; +import { waypointFromYMap } from "~/lib/waypoint-ymap"; interface SaveToJournalButtonProps { yjs: YjsState; @@ -42,14 +42,7 @@ export function SaveToJournalButton({ yjs, callbackUrl, callbackToken, returnUrl points: (yMap.get("points") as Array<{ lat: number; lon: number }>) ?? [], })).filter((a) => a.points.length >= 3); - const waypoints = yjs.waypoints.toArray().map((yMap: Y.Map) => ({ - lat: yMap.get("lat") as number, - lon: yMap.get("lon") as number, - name: yMap.get("name") as string | undefined, - isDayBreak: yMap.get("overnight") === true ? true : undefined, - osmId: yMap.get("osmId") as number | undefined, - poiTags: yMap.get("poiTags") as WaypointPoiTags | undefined, - })); + const waypoints = yjs.waypoints.toArray().map(waypointFromYMap); const notes = yjs.notes.toString() || undefined; const gpx = generateGpx({ name: "trails.cool route", description: notes, waypoints, tracks, noGoAreas }); diff --git a/apps/planner/app/components/WaypointSidebar.tsx b/apps/planner/app/components/WaypointSidebar.tsx index cabcab6..baaf992 100644 --- a/apps/planner/app/components/WaypointSidebar.tsx +++ b/apps/planner/app/components/WaypointSidebar.tsx @@ -8,6 +8,7 @@ import { DayBreakdown } from "./DayBreakdown"; import { useNearbyPois } from "~/lib/use-nearby-pois"; import { poiCategories } from "@trails-cool/map-core"; import type { Poi } from "~/lib/overpass"; +import { waypointFromYMap } from "~/lib/waypoint-ymap"; const NOTE_MAX = 500; @@ -20,13 +21,10 @@ interface WaypointData { } function getWaypointsFromYjs(waypoints: Y.Array>): WaypointData[] { - return waypoints.toArray().map((yMap) => ({ - 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, - overnight: isOvernight(yMap), - })); + return waypoints.toArray().map((yMap) => { + const wp = waypointFromYMap(yMap); + return { lat: wp.lat, lon: wp.lon, name: wp.name, note: wp.note, overnight: isOvernight(yMap) }; + }); } interface WaypointSidebarProps { diff --git a/apps/planner/app/lib/use-gpx-drop.ts b/apps/planner/app/lib/use-gpx-drop.ts index 32cd4ce..506f618 100644 --- a/apps/planner/app/lib/use-gpx-drop.ts +++ b/apps/planner/app/lib/use-gpx-drop.ts @@ -3,6 +3,7 @@ import * as Y from "yjs"; import { useTranslation } from "react-i18next"; import { parseGpxAsync, extractWaypoints } from "@trails-cool/gpx"; import type { YjsState } from "~/lib/use-yjs"; +import { waypointToYMap } from "~/lib/waypoint-ymap"; export function useGpxDrop(yjs: YjsState, onImportError?: (message: string) => void) { const { t } = useTranslation("planner"); @@ -48,13 +49,7 @@ export function useGpxDrop(yjs: YjsState, onImportError?: (message: string) => v yjs.doc.transact(() => { yjs.waypoints.delete(0, yjs.waypoints.length); for (const wp of newWaypoints) { - const yMap = new Y.Map(); - yMap.set("lat", wp.lat); - yMap.set("lon", wp.lon); - if (wp.name) yMap.set("name", wp.name); - if (wp.isDayBreak) yMap.set("overnight", true); - if (wp.note) yMap.set("note", wp.note); - yjs.waypoints.push([yMap]); + yjs.waypoints.push([waypointToYMap(wp)]); } yjs.noGoAreas.delete(0, yjs.noGoAreas.length); diff --git a/apps/planner/app/lib/use-waypoint-manager.ts b/apps/planner/app/lib/use-waypoint-manager.ts index bc6e204..866e27d 100644 --- a/apps/planner/app/lib/use-waypoint-manager.ts +++ b/apps/planner/app/lib/use-waypoint-manager.ts @@ -6,6 +6,7 @@ import { usePois } from "~/lib/use-pois"; import { snapToPoi } from "~/lib/poi-snap"; import { isOvernight } from "~/lib/overnight"; import { findSegmentForPoint } from "~/components/ColoredRoute"; +import { waypointFromYMap } from "~/lib/waypoint-ymap"; export interface WaypointData { lat: number; @@ -16,13 +17,10 @@ export interface WaypointData { } function getWaypointsFromYjs(waypoints: Y.Array>): WaypointData[] { - return waypoints.toArray().map((yMap) => ({ - 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, - overnight: isOvernight(yMap), - })); + return waypoints.toArray().map((yMap) => { + const wp = waypointFromYMap(yMap); + return { lat: wp.lat, lon: wp.lon, name: wp.name, note: wp.note, overnight: isOvernight(yMap) }; + }); } function pointToSegmentDist( diff --git a/apps/planner/app/lib/use-yjs.ts b/apps/planner/app/lib/use-yjs.ts index 2681edc..a9637f6 100644 --- a/apps/planner/app/lib/use-yjs.ts +++ b/apps/planner/app/lib/use-yjs.ts @@ -1,6 +1,8 @@ import { useEffect, useRef, useState } from "react"; import * as Y from "yjs"; import { WebsocketProvider } from "y-websocket"; +import type { Waypoint } from "@trails-cool/types"; +import { waypointToYMap } from "~/lib/waypoint-ymap"; const COLORS = [ "#ef4444", "#f97316", "#eab308", "#22c55e", @@ -42,7 +44,7 @@ export interface YjsState { export function useYjs( sessionId: string, - initialWaypoints?: Array<{ lat: number; lon: number; name?: string; isDayBreak?: boolean }>, + initialWaypoints?: Waypoint[], initialNoGoAreas?: Array<{ points: Array<{ lat: number; lon: number }> }>, initialNotes?: string, ): YjsState | null { @@ -103,12 +105,7 @@ export function useYjs( initializedWaypoints.current = true; doc.transact(() => { for (const wp of initialWaypoints) { - const yMap = new Y.Map(); - yMap.set("lat", wp.lat); - yMap.set("lon", wp.lon); - if (wp.name) yMap.set("name", wp.name); - if (wp.isDayBreak) yMap.set("overnight", true); - waypoints.push([yMap]); + waypoints.push([waypointToYMap(wp)]); } if (initialNoGoAreas?.length && noGoAreas.length === 0) { for (const area of initialNoGoAreas) { diff --git a/apps/planner/app/lib/waypoint-ymap.ts b/apps/planner/app/lib/waypoint-ymap.ts new file mode 100644 index 0000000..d2ac67c --- /dev/null +++ b/apps/planner/app/lib/waypoint-ymap.ts @@ -0,0 +1,41 @@ +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): 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, 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 { + const yMap = new Y.Map(); + applyWaypointToYMap(yMap, wp); + return yMap; +} diff --git a/apps/planner/app/routes/api.sessions.ts b/apps/planner/app/routes/api.sessions.ts index df4e17c..3aee552 100644 --- a/apps/planner/app/routes/api.sessions.ts +++ b/apps/planner/app/routes/api.sessions.ts @@ -3,6 +3,7 @@ import type { Route } from "./+types/api.sessions"; import { createSession, listSessions } from "~/lib/sessions"; import { parseGpxAsync, extractWaypoints } from "@trails-cool/gpx"; import { withDb } from "@trails-cool/db"; +import type { Waypoint } from "@trails-cool/types"; export async function action({ request }: Route.ActionArgs) { if (request.method !== "POST") { @@ -19,7 +20,7 @@ export async function action({ request }: Route.ActionArgs) { return withDb(async () => { const session = await createSession({ callbackUrl, callbackToken }); - let initialWaypoints: Array<{ lat: number; lon: number; name?: string; isDayBreak?: boolean }> | undefined; + let initialWaypoints: Waypoint[] | undefined; let initialNoGoAreas: Array<{ points: Array<{ lat: number; lon: number }> }> | undefined; let initialNotes: string | undefined; if (gpx) {