From e158205f32e62bf83faccc8f052b92bb0f5791a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sat, 11 Apr 2026 02:32:36 +0200 Subject: [PATCH] Sync tile overlay state via Yjs across participants OverlaySync component listens to Leaflet overlayadd/overlayremove events and writes enabled overlay IDs to Yjs routeData. On connect, loads initial overlay state from Yjs. LayersControl.Overlay uses checked prop to reflect the synced state. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/planner/app/components/PlannerMap.tsx | 62 +++++++++++++++++++++- openspec/changes/osm-overlays/tasks.md | 2 +- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/apps/planner/app/components/PlannerMap.tsx b/apps/planner/app/components/PlannerMap.tsx index ecdc8dc..8bdc5b2 100644 --- a/apps/planner/app/components/PlannerMap.tsx +++ b/apps/planner/app/components/PlannerMap.tsx @@ -237,6 +237,64 @@ function NoGoAreaButton({ active, onClick }: { active: boolean; onClick: () => v ); } +function OverlaySync({ yjs, onOverlayChange }: { yjs: YjsState; onOverlayChange: (ids: string[]) => void }) { + const map = useMap(); + const suppressRef = useRef(false); + + // Map events → Yjs + useEffect(() => { + const handleAdd = (e: L.LayersControlEvent) => { + if (suppressRef.current) return; + const name = e.name; + const layer = overlayLayers.find((l) => l.name === name); + if (!layer) return; + const raw = yjs.routeData.get("overlays") as string | undefined; + const current: string[] = raw ? JSON.parse(raw) : []; + if (!current.includes(layer.id)) { + const updated = [...current, layer.id]; + yjs.routeData.set("overlays", JSON.stringify(updated)); + onOverlayChange(updated); + } + }; + + const handleRemove = (e: L.LayersControlEvent) => { + if (suppressRef.current) return; + const name = e.name; + const layer = overlayLayers.find((l) => l.name === name); + if (!layer) return; + const raw = yjs.routeData.get("overlays") as string | undefined; + const current: string[] = raw ? JSON.parse(raw) : []; + const updated = current.filter((id) => id !== layer.id); + yjs.routeData.set("overlays", JSON.stringify(updated)); + onOverlayChange(updated); + }; + + map.on("overlayadd", handleAdd as L.LeafletEventHandlerFn); + map.on("overlayremove", handleRemove as L.LeafletEventHandlerFn); + return () => { + map.off("overlayadd", handleAdd as L.LeafletEventHandlerFn); + map.off("overlayremove", handleRemove as L.LeafletEventHandlerFn); + }; + }, [map, yjs, onOverlayChange]); + + // Yjs → Map: load initial state from Yjs + useEffect(() => { + const handleChange = () => { + const raw = yjs.routeData.get("overlays") as string | undefined; + if (!raw) return; + try { + const ids: string[] = JSON.parse(raw); + onOverlayChange(ids); + } catch { /* ignore */ } + }; + yjs.routeData.observe(handleChange); + handleChange(); + return () => yjs.routeData.unobserve(handleChange); + }, [yjs, onOverlayChange]); + + return null; +} + function PoiRefresher({ poiState }: { poiState: ReturnType }) { const map = useMap(); const refreshRef = useRef(poiState.refresh); @@ -282,6 +340,7 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition, highlighted const poiState = usePois(); useProfileDefaults(yjs, poiState); useYjsPoiSync(yjs, poiState); + const [enabledOverlays, setEnabledOverlays] = useState([]); const [draggingOver, setDraggingOver] = useState(false); const dragCounterRef = useRef(0); const [routeCoordinates, setRouteCoordinates] = useState<[number, number, number][] | null>(null); @@ -526,13 +585,14 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition, highlighted ))} {overlayLayers.map((layer) => ( - + ))} + {} : addWaypoint} suppressRef={suppressMapClickRef} /> diff --git a/openspec/changes/osm-overlays/tasks.md b/openspec/changes/osm-overlays/tasks.md index f98e686..438d23d 100644 --- a/openspec/changes/osm-overlays/tasks.md +++ b/openspec/changes/osm-overlays/tasks.md @@ -6,7 +6,7 @@ ## 2. Overlay State Sync -- [ ] 2.1 Add `overlays` string array to Yjs `routeOptions` Y.Map for enabled tile overlay IDs (deferred — tile overlays are visual-only) +- [x] 2.1 Add `overlays` string array to Yjs `routeOptions` Y.Map for enabled tile overlay IDs - [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