import { useEffect, useState, useCallback, useRef } from "react"; import { MapContainer, TileLayer, LayersControl, Marker, CircleMarker, useMapEvents, useMap } from "react-leaflet"; import L from "leaflet"; import * as Y from "yjs"; import type { YjsState } from "~/lib/use-yjs"; import { baseLayers } from "@trails-cool/map"; import { NoGoAreaLayer } from "./NoGoAreaLayer"; import { ColoredRoute, findSegmentForPoint, type ColorMode } from "./ColoredRoute"; import { RouteInteraction } from "./RouteInteraction"; import "leaflet/dist/leaflet.css"; function waypointIcon(index: number): L.DivIcon { return L.divIcon({ className: "", html: `
${index + 1}
`, iconSize: [0, 0], }); } interface WaypointData { lat: number; lon: number; name?: string; } 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, })); } interface PlannerMapProps { yjs: YjsState; onRouteRequest?: (waypoints: WaypointData[]) => void; highlightPosition?: [number, number] | null; } function MapExposer() { const map = useMap(); useEffect(() => { if (typeof window !== "undefined") { (window as unknown as Record).__leafletMap = map; } }, [map]); return null; } function MapClickHandler({ onAdd, suppressRef }: { onAdd: (lat: number, lng: number) => void; suppressRef: React.RefObject }) { useMapEvents({ click(e) { if (suppressRef.current) { suppressRef.current = false; return; } onAdd(e.latlng.lat, e.latlng.lng); }, }); return null; } function CursorTracker({ awareness }: { awareness: YjsState["awareness"] }) { const map = useMap(); const [cursors, setCursors] = useState>(new Map()); useEffect(() => { const localId = awareness.clientID; const handleMouseMove = (e: L.LeafletMouseEvent) => { awareness.setLocalStateField("cursor", { lat: e.latlng.lat, lng: e.latlng.lng, }); }; const handleMouseOut = () => { awareness.setLocalStateField("cursor", null); }; map.on("mousemove", handleMouseMove); map.on("mouseout", handleMouseOut); const updateCursors = () => { const states = awareness.getStates(); const newCursors = new Map(); states.forEach((state, clientId) => { if (clientId !== localId && state.cursor && state.user) { newCursors.set(clientId, { lat: state.cursor.lat, lng: state.cursor.lng, color: state.user.color, name: state.user.name, }); } }); setCursors(newCursors); }; awareness.on("change", updateCursors); return () => { map.off("mousemove", handleMouseMove); map.off("mouseout", handleMouseOut); awareness.off("change", updateCursors); }; }, [map, awareness]); return ( <> {Array.from(cursors.entries()).map(([clientId, cursor]) => ( ${cursor.name} `, iconSize: [0, 0], })} /> ))} ); } function NoGoAreaButton({ active, onClick }: { active: boolean; onClick: () => void }) { return ( ); } export function PlannerMap({ yjs, onRouteRequest, highlightPosition }: PlannerMapProps) { const [waypoints, setWaypoints] = useState([]); const [routeCoordinates, setRouteCoordinates] = useState<[number, number, number][] | null>(null); const [segmentBoundaries, setSegmentBoundaries] = useState([]); const [surfaces, setSurfaces] = useState([]); const [colorMode, setColorMode] = useState("plain"); const [noGoDrawing, setNoGoDrawing] = useState(false); const toggleNoGoDraw = useCallback(() => setNoGoDrawing((v) => !v), []); const suppressMapClickRef = useRef(false); // Sync waypoints from Yjs useEffect(() => { const update = () => { const wps = getWaypointsFromYjs(yjs.waypoints); setWaypoints(wps); if (wps.length >= 2 && onRouteRequest) { onRouteRequest(wps); } }; yjs.waypoints.observeDeep(update); update(); return () => { yjs.waypoints.unobserveDeep(update); }; }, [yjs.waypoints, onRouteRequest]); // Sync route data from Yjs (enriched: coordinates + segment boundaries) useEffect(() => { const update = () => { const coordsJson = yjs.routeData.get("coordinates") as string | undefined; const boundsJson = yjs.routeData.get("segmentBoundaries") as string | undefined; const modeVal = yjs.routeData.get("colorMode") as ColorMode | undefined; if (coordsJson) { try { setRouteCoordinates(JSON.parse(coordsJson)); } catch { setRouteCoordinates(null); } } else { // Fallback: parse from geojson for backwards compat const geojson = yjs.routeData.get("geojson") as string | undefined; if (geojson) { try { const parsed = JSON.parse(geojson); const coords = parsed.features?.[0]?.geometry?.coordinates; if (coords) { setRouteCoordinates(coords.map((c: number[]) => [c[0]!, c[1]!, c[2] ?? 0] as [number, number, number])); } } catch { setRouteCoordinates(null); } } else { setRouteCoordinates(null); } } if (boundsJson) { try { setSegmentBoundaries(JSON.parse(boundsJson)); } catch { setSegmentBoundaries([]); } } else { setSegmentBoundaries([]); } const surfacesJson = yjs.routeData.get("surfaces") as string | undefined; if (surfacesJson) { try { setSurfaces(JSON.parse(surfacesJson)); } catch { setSurfaces([]); } } else { setSurfaces([]); } if (modeVal) setColorMode(modeVal); }; yjs.routeData.observe(update); update(); return () => { yjs.routeData.unobserve(update); }; }, [yjs.routeData]); const addWaypoint = useCallback( (lat: number, lng: number) => { const yMap = new Y.Map(); yMap.set("lat", lat); yMap.set("lon", lng); yjs.waypoints.push([yMap]); }, [yjs.waypoints], ); const insertWaypointAtSegment = useCallback( (segmentIndex: number, lat: number, lon: number) => { const yMap = new Y.Map(); yMap.set("lat", lat); yMap.set("lon", lon); // Insert after the segment's start waypoint yjs.waypoints.insert(segmentIndex + 1, [yMap]); }, [yjs.waypoints], ); const handleRouteInsert = useCallback( (pointIndex: number, lat: number, lon: number) => { suppressMapClickRef.current = true; const segIdx = findSegmentForPoint(pointIndex, segmentBoundaries); insertWaypointAtSegment(segIdx, lat, lon); }, [segmentBoundaries, insertWaypointAtSegment], ); const moveWaypoint = useCallback( (index: number, lat: number, lng: number) => { const yMap = yjs.waypoints.get(index); if (yMap) { yjs.doc.transact(() => { yMap.set("lat", lat); yMap.set("lon", lng); }); } }, [yjs.waypoints, yjs.doc], ); const deleteWaypoint = useCallback( (index: number) => { yjs.waypoints.delete(index, 1); }, [yjs.waypoints], ); return ( {baseLayers.map((layer, i) => ( ))} {} : addWaypoint} suppressRef={suppressMapClickRef} /> {waypoints.map((wp, i) => ( { const { lat, lng } = e.target.getLatLng(); moveWaypoint(i, lat, lng); }, contextmenu: (e) => { L.DomEvent.preventDefault(e as unknown as Event); deleteWaypoint(i); }, }} /> ))} {routeCoordinates && routeCoordinates.length >= 2 && ( <> )} {highlightPosition && ( )} ); }