Centralize waypoint Yjs serialization in waypointFromYMap/waypointToYMap
Introduce waypoint-ymap.ts with typed helpers so all Yjs↔Waypoint conversions go through one place. New Waypoint fields now only need to be added once rather than in every consumer. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
02f8a8be44
commit
9e2ca5595e
8 changed files with 63 additions and 52 deletions
|
|
@ -4,7 +4,7 @@ import * as Y from "yjs";
|
||||||
import type { YjsState } from "~/lib/use-yjs";
|
import type { YjsState } from "~/lib/use-yjs";
|
||||||
import { generateGpx, computeDays } from "@trails-cool/gpx";
|
import { generateGpx, computeDays } from "@trails-cool/gpx";
|
||||||
import type { TrackPoint, NoGoArea } 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[][] {
|
function getTracks(yjs: YjsState): TrackPoint[][] {
|
||||||
const geojsonStr = yjs.routeData.get("geojson") as string | undefined;
|
const geojsonStr = yjs.routeData.get("geojson") as string | undefined;
|
||||||
|
|
@ -20,15 +20,7 @@ function getTracks(yjs: YjsState): TrackPoint[][] {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWaypoints(yjs: YjsState) {
|
function getWaypoints(yjs: YjsState) {
|
||||||
return yjs.waypoints.toArray().map((yMap: Y.Map<unknown>) => ({
|
return yjs.waypoints.toArray().map(waypointFromYMap);
|
||||||
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,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNoGoAreas(yjs: YjsState): NoGoArea[] {
|
function getNoGoAreas(yjs: YjsState): NoGoArea[] {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import * as Y from "yjs";
|
||||||
import type { YjsState } from "~/lib/use-yjs";
|
import type { YjsState } from "~/lib/use-yjs";
|
||||||
import { generateGpx } from "@trails-cool/gpx";
|
import { generateGpx } from "@trails-cool/gpx";
|
||||||
import type { TrackPoint, NoGoArea } 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 {
|
interface SaveToJournalButtonProps {
|
||||||
yjs: YjsState;
|
yjs: YjsState;
|
||||||
|
|
@ -42,15 +42,7 @@ export function SaveToJournalButton({ yjs, callbackUrl, callbackToken, returnUrl
|
||||||
points: (yMap.get("points") as Array<{ lat: number; lon: number }>) ?? [],
|
points: (yMap.get("points") as Array<{ lat: number; lon: number }>) ?? [],
|
||||||
})).filter((a) => a.points.length >= 3);
|
})).filter((a) => a.points.length >= 3);
|
||||||
|
|
||||||
const waypoints = yjs.waypoints.toArray().map((yMap: Y.Map<unknown>) => ({
|
const waypoints = yjs.waypoints.toArray().map(waypointFromYMap);
|
||||||
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,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const notes = yjs.notes.toString() || undefined;
|
const notes = yjs.notes.toString() || undefined;
|
||||||
const gpx = generateGpx({ name: "trails.cool route", description: notes, waypoints, tracks, noGoAreas });
|
const gpx = generateGpx({ name: "trails.cool route", description: notes, waypoints, tracks, noGoAreas });
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { DayBreakdown } from "./DayBreakdown";
|
||||||
import { useNearbyPois } from "~/lib/use-nearby-pois";
|
import { useNearbyPois } from "~/lib/use-nearby-pois";
|
||||||
import { poiCategories } from "@trails-cool/map-core";
|
import { poiCategories } from "@trails-cool/map-core";
|
||||||
import type { Poi } from "~/lib/overpass";
|
import type { Poi } from "~/lib/overpass";
|
||||||
|
import { waypointFromYMap } from "~/lib/waypoint-ymap";
|
||||||
|
|
||||||
const NOTE_MAX = 500;
|
const NOTE_MAX = 500;
|
||||||
|
|
||||||
|
|
@ -20,13 +21,10 @@ interface WaypointData {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWaypointsFromYjs(waypoints: Y.Array<Y.Map<unknown>>): WaypointData[] {
|
function getWaypointsFromYjs(waypoints: Y.Array<Y.Map<unknown>>): WaypointData[] {
|
||||||
return waypoints.toArray().map((yMap) => ({
|
return waypoints.toArray().map((yMap) => {
|
||||||
lat: yMap.get("lat") as number,
|
const wp = waypointFromYMap(yMap);
|
||||||
lon: yMap.get("lon") as number,
|
return { lat: wp.lat, lon: wp.lon, name: wp.name, note: wp.note, overnight: isOvernight(yMap) };
|
||||||
name: yMap.get("name") as string | undefined,
|
});
|
||||||
note: yMap.get("note") as string | undefined,
|
|
||||||
overnight: isOvernight(yMap),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WaypointSidebarProps {
|
interface WaypointSidebarProps {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import * as Y from "yjs";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { parseGpxAsync, extractWaypoints } from "@trails-cool/gpx";
|
import { parseGpxAsync, extractWaypoints } from "@trails-cool/gpx";
|
||||||
import type { YjsState } from "~/lib/use-yjs";
|
import type { YjsState } from "~/lib/use-yjs";
|
||||||
|
import { waypointToYMap } from "~/lib/waypoint-ymap";
|
||||||
|
|
||||||
export function useGpxDrop(yjs: YjsState, onImportError?: (message: string) => void) {
|
export function useGpxDrop(yjs: YjsState, onImportError?: (message: string) => void) {
|
||||||
const { t } = useTranslation("planner");
|
const { t } = useTranslation("planner");
|
||||||
|
|
@ -48,13 +49,7 @@ export function useGpxDrop(yjs: YjsState, onImportError?: (message: string) => v
|
||||||
yjs.doc.transact(() => {
|
yjs.doc.transact(() => {
|
||||||
yjs.waypoints.delete(0, yjs.waypoints.length);
|
yjs.waypoints.delete(0, yjs.waypoints.length);
|
||||||
for (const wp of newWaypoints) {
|
for (const wp of newWaypoints) {
|
||||||
const yMap = new Y.Map();
|
yjs.waypoints.push([waypointToYMap(wp)]);
|
||||||
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.noGoAreas.delete(0, yjs.noGoAreas.length);
|
yjs.noGoAreas.delete(0, yjs.noGoAreas.length);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { usePois } from "~/lib/use-pois";
|
||||||
import { snapToPoi } from "~/lib/poi-snap";
|
import { snapToPoi } from "~/lib/poi-snap";
|
||||||
import { isOvernight } from "~/lib/overnight";
|
import { isOvernight } from "~/lib/overnight";
|
||||||
import { findSegmentForPoint } from "~/components/ColoredRoute";
|
import { findSegmentForPoint } from "~/components/ColoredRoute";
|
||||||
|
import { waypointFromYMap } from "~/lib/waypoint-ymap";
|
||||||
|
|
||||||
export interface WaypointData {
|
export interface WaypointData {
|
||||||
lat: number;
|
lat: number;
|
||||||
|
|
@ -16,13 +17,10 @@ export interface WaypointData {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWaypointsFromYjs(waypoints: Y.Array<Y.Map<unknown>>): WaypointData[] {
|
function getWaypointsFromYjs(waypoints: Y.Array<Y.Map<unknown>>): WaypointData[] {
|
||||||
return waypoints.toArray().map((yMap) => ({
|
return waypoints.toArray().map((yMap) => {
|
||||||
lat: yMap.get("lat") as number,
|
const wp = waypointFromYMap(yMap);
|
||||||
lon: yMap.get("lon") as number,
|
return { lat: wp.lat, lon: wp.lon, name: wp.name, note: wp.note, overnight: isOvernight(yMap) };
|
||||||
name: yMap.get("name") as string | undefined,
|
});
|
||||||
note: yMap.get("note") as string | undefined,
|
|
||||||
overnight: isOvernight(yMap),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pointToSegmentDist(
|
function pointToSegmentDist(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import * as Y from "yjs";
|
import * as Y from "yjs";
|
||||||
import { WebsocketProvider } from "y-websocket";
|
import { WebsocketProvider } from "y-websocket";
|
||||||
|
import type { Waypoint } from "@trails-cool/types";
|
||||||
|
import { waypointToYMap } from "~/lib/waypoint-ymap";
|
||||||
|
|
||||||
const COLORS = [
|
const COLORS = [
|
||||||
"#ef4444", "#f97316", "#eab308", "#22c55e",
|
"#ef4444", "#f97316", "#eab308", "#22c55e",
|
||||||
|
|
@ -42,7 +44,7 @@ export interface YjsState {
|
||||||
|
|
||||||
export function useYjs(
|
export function useYjs(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
initialWaypoints?: Array<{ lat: number; lon: number; name?: string; isDayBreak?: boolean; note?: string; osmId?: number; poiTags?: Record<string, string> }>,
|
initialWaypoints?: Waypoint[],
|
||||||
initialNoGoAreas?: Array<{ points: Array<{ lat: number; lon: number }> }>,
|
initialNoGoAreas?: Array<{ points: Array<{ lat: number; lon: number }> }>,
|
||||||
initialNotes?: string,
|
initialNotes?: string,
|
||||||
): YjsState | null {
|
): YjsState | null {
|
||||||
|
|
@ -103,15 +105,7 @@ export function useYjs(
|
||||||
initializedWaypoints.current = true;
|
initializedWaypoints.current = true;
|
||||||
doc.transact(() => {
|
doc.transact(() => {
|
||||||
for (const wp of initialWaypoints) {
|
for (const wp of initialWaypoints) {
|
||||||
const yMap = new Y.Map();
|
waypoints.push([waypointToYMap(wp)]);
|
||||||
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);
|
|
||||||
if (wp.osmId !== undefined) yMap.set("osmId", wp.osmId);
|
|
||||||
if (wp.poiTags) yMap.set("poiTags", wp.poiTags);
|
|
||||||
waypoints.push([yMap]);
|
|
||||||
}
|
}
|
||||||
if (initialNoGoAreas?.length && noGoAreas.length === 0) {
|
if (initialNoGoAreas?.length && noGoAreas.length === 0) {
|
||||||
for (const area of initialNoGoAreas) {
|
for (const area of initialNoGoAreas) {
|
||||||
|
|
|
||||||
41
apps/planner/app/lib/waypoint-ymap.ts
Normal file
41
apps/planner/app/lib/waypoint-ymap.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import type { Route } from "./+types/api.sessions";
|
||||||
import { createSession, listSessions } from "~/lib/sessions";
|
import { createSession, listSessions } from "~/lib/sessions";
|
||||||
import { parseGpxAsync, extractWaypoints } from "@trails-cool/gpx";
|
import { parseGpxAsync, extractWaypoints } from "@trails-cool/gpx";
|
||||||
import { withDb } from "@trails-cool/db";
|
import { withDb } from "@trails-cool/db";
|
||||||
|
import type { Waypoint } from "@trails-cool/types";
|
||||||
|
|
||||||
export async function action({ request }: Route.ActionArgs) {
|
export async function action({ request }: Route.ActionArgs) {
|
||||||
if (request.method !== "POST") {
|
if (request.method !== "POST") {
|
||||||
|
|
@ -19,7 +20,7 @@ export async function action({ request }: Route.ActionArgs) {
|
||||||
return withDb(async () => {
|
return withDb(async () => {
|
||||||
const session = await createSession({ callbackUrl, callbackToken });
|
const session = await createSession({ callbackUrl, callbackToken });
|
||||||
|
|
||||||
let initialWaypoints: Array<{ lat: number; lon: number; name?: string; isDayBreak?: boolean; note?: string; osmId?: number; poiTags?: Record<string, string> }> | undefined;
|
let initialWaypoints: Waypoint[] | undefined;
|
||||||
let initialNoGoAreas: Array<{ points: Array<{ lat: number; lon: number }> }> | undefined;
|
let initialNoGoAreas: Array<{ points: Array<{ lat: number; lon: number }> }> | undefined;
|
||||||
let initialNotes: string | undefined;
|
let initialNotes: string | undefined;
|
||||||
if (gpx) {
|
if (gpx) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue