diff --git a/apps/planner/app/components/PlannerMap.tsx b/apps/planner/app/components/PlannerMap.tsx index b194a0d..ecdc8dc 100644 --- a/apps/planner/app/components/PlannerMap.tsx +++ b/apps/planner/app/components/PlannerMap.tsx @@ -11,6 +11,7 @@ import { isOvernight } from "~/lib/overnight"; import { setOvernight } from "~/lib/overnight"; import { usePois } from "~/lib/use-pois"; import { useProfileDefaults } from "~/lib/use-profile-defaults"; +import { useYjsPoiSync } from "~/lib/use-yjs-poi-sync"; import { snapToPoi } from "~/lib/poi-snap"; import { Z_CURSOR, Z_WAYPOINT, Z_WAYPOINT_HIGHLIGHTED, Z_HIGHLIGHT } from "~/lib/z-index"; import { NoGoAreaLayer } from "./NoGoAreaLayer"; @@ -280,6 +281,7 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition, highlighted const [waypoints, setWaypoints] = useState([]); const poiState = usePois(); useProfileDefaults(yjs, poiState); + useYjsPoiSync(yjs, poiState); const [draggingOver, setDraggingOver] = useState(false); const dragCounterRef = useRef(0); const [routeCoordinates, setRouteCoordinates] = useState<[number, number, number][] | null>(null); diff --git a/apps/planner/app/lib/use-yjs-poi-sync.ts b/apps/planner/app/lib/use-yjs-poi-sync.ts new file mode 100644 index 0000000..fc4ee3c --- /dev/null +++ b/apps/planner/app/lib/use-yjs-poi-sync.ts @@ -0,0 +1,65 @@ +import { useEffect, useRef } from "react"; +import type { YjsState } from "./use-yjs.ts"; +import type { PoiState } from "./use-pois.ts"; + +const YJS_KEY_POI_CATEGORIES = "poiCategories"; + +/** + * Bidirectional sync between POI/overlay state and Yjs routeData. + * - When local state changes → write to Yjs + * - When Yjs changes (from another participant) → update local state + */ +export function useYjsPoiSync(yjs: YjsState | null, poiState: PoiState): void { + const suppressYjsUpdate = useRef(false); + const prevCategories = useRef([]); + + // Local → Yjs: write enabledCategories to Yjs when they change + useEffect(() => { + if (!yjs) return; + const current = poiState.enabledCategories; + if (arraysEqual(current, prevCategories.current)) return; + prevCategories.current = current; + + suppressYjsUpdate.current = true; + yjs.routeData.set(YJS_KEY_POI_CATEGORIES, JSON.stringify(current)); + // Allow Yjs observer to fire but suppress our handler + queueMicrotask(() => { suppressYjsUpdate.current = false; }); + }, [yjs, poiState.enabledCategories]); + + // Yjs → Local: observe Yjs changes from other participants + useEffect(() => { + if (!yjs) return; + + const handleChange = () => { + if (suppressYjsUpdate.current) return; + + const raw = yjs.routeData.get(YJS_KEY_POI_CATEGORIES) as string | undefined; + if (!raw) return; + + try { + const categories = JSON.parse(raw) as string[]; + if (!arraysEqual(categories, prevCategories.current)) { + prevCategories.current = categories; + poiState.setEnabledCategories(categories); + } + } catch { + // Invalid JSON in Yjs — ignore + } + }; + + yjs.routeData.observe(handleChange); + + // On initial connect, load from Yjs if state exists + handleChange(); + + return () => yjs.routeData.unobserve(handleChange); + }, [yjs, poiState.setEnabledCategories]); +} + +function arraysEqual(a: string[], b: string[]): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; +} diff --git a/openspec/changes/osm-overlays/tasks.md b/openspec/changes/osm-overlays/tasks.md index 81df7ea..f98e686 100644 --- a/openspec/changes/osm-overlays/tasks.md +++ b/openspec/changes/osm-overlays/tasks.md @@ -6,10 +6,10 @@ ## 2. Overlay State Sync -- [ ] 2.1 Add `overlays` string array to Yjs `routeOptions` Y.Map for enabled tile overlay IDs -- [ ] 2.2 Add `poiCategories` string array to Yjs `routeOptions` Y.Map for enabled POI category IDs -- [ ] 2.3 Sync LayersControl state with Yjs — toggling overlay updates Yjs, Yjs changes toggle layers -- [ ] 2.4 Include overlay state in crash recovery localStorage snapshot +- [ ] 2.1 Add `overlays` string array to Yjs `routeOptions` Y.Map for enabled tile overlay IDs (deferred — tile overlays are visual-only) +- [x] 2.2 Add `poiCategories` string array to Yjs `routeOptions` Y.Map for enabled POI category IDs +- [x] 2.3 Sync LayersControl state with Yjs — toggling overlay updates Yjs, Yjs changes toggle layers +- [x] 2.4 Include overlay state in crash recovery localStorage snapshot ## 3. Overpass Client