import { useEffect, useState, useCallback } from "react"; import { MapContainer, TileLayer, LayersControl, Marker, Polyline, 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 "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 MapClickHandler({ onAdd }: { onAdd: (lat: number, lng: number) => void }) { useMapEvents({ click(e) { 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], })} /> ))} ); } export function PlannerMap({ yjs, onRouteRequest, highlightPosition }: PlannerMapProps) { const [waypoints, setWaypoints] = useState([]); const [routeGeoJson, setRouteGeoJson] = useState(null); // 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 useEffect(() => { const update = () => { 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) { setRouteGeoJson(coords.map((c: number[]) => [c[1], c[0]] as L.LatLngExpression)); } } catch { // Invalid GeoJSON } } else { setRouteGeoJson(null); } }; 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 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) => ( ))} {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); }, }} /> ))} {routeGeoJson && } {highlightPosition && ( )} ); }