Merge pull request #394 from trails-cool/stigi/waypoint-notes-journal

Propagate waypoint notes and POI data through full round-trip
This commit is contained in:
Ullrich Schäfer 2026-05-18 21:07:27 +02:00 committed by GitHub
commit 72c01eb18c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 71 additions and 52 deletions

View file

@ -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<string, string> }>;
initialNoGoAreas?: Array<{ points: Array<{ lat: number; lon: number }> }>;
initialNotes?: string;
};

View file

@ -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<string, string> }> = [];
let waypoints: Array<{ lat: number; lon: number; name?: string; isDayBreak?: boolean; note?: string; osmId?: number; poiTags?: Record<string, string> }> = [];
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<string, string> | undefined,
}));
@ -398,15 +399,18 @@ export default function RouteDetailPage({ loaderData }: Route.ComponentProps) {
</div>
)}
{waypoints.some((w) => w.osmId || w.poiTags) && (
{waypoints.some((w) => w.osmId || w.poiTags || w.note) && (
<div className="mt-6">
<h2 className="text-lg font-semibold text-gray-900">{t("routes.waypoints")}</h2>
<ul className="mt-3 divide-y divide-gray-200 rounded-md border border-gray-200">
{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) => (
<li key={i} className="px-4 py-3">
<p className="font-medium text-gray-900">
{w.name ?? `${w.lat.toFixed(5)}, ${w.lon.toFixed(5)}`}
</p>
{w.note && (
<p className="mt-1 whitespace-pre-wrap text-sm text-gray-600">{w.note}</p>
)}
{w.poiTags && (
<dl className="mt-1 space-y-0.5 text-sm text-gray-600">
{w.poiTags.phone && (

View file

@ -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<unknown>) => ({
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[] {

View file

@ -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<unknown>) => ({
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 });

View file

@ -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<Y.Map<unknown>>): 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 {

View file

@ -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);

View file

@ -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<Y.Map<unknown>>): 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(

View file

@ -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) {

View file

@ -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<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;
}

View file

@ -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) {