import { useEffect, useState, useCallback } from "react"; import * as Y from "yjs"; import type { YjsState } from "~/lib/use-yjs"; import { usePois } from "~/lib/use-pois"; import { snapToPoi } from "~/lib/poi-snap"; import { findSegmentForPoint } from "~/components/ColoredRoute"; import { extractWaypointData, type WaypointData } from "~/lib/waypoint-ymap"; import { getColorMode, readComputedRoute, type ColorMode, } from "~/lib/route-data"; export type { WaypointData }; function pointToSegmentDist( pLat: number, pLon: number, aLat: number, aLon: number, bLat: number, bLon: number, ): number { const dx = bLon - aLon; const dy = bLat - aLat; const lenSq = dx * dx + dy * dy; if (lenSq === 0) return Math.sqrt((pLon - aLon) ** 2 + (pLat - aLat) ** 2); const t = Math.max(0, Math.min(1, ((pLon - aLon) * dx + (pLat - aLat) * dy) / lenSq)); const projLon = aLon + t * dx; const projLat = aLat + t * dy; return Math.sqrt((pLon - projLon) ** 2 + (pLat - projLat) ** 2); } export interface RouteState { waypoints: WaypointData[]; routeCoordinates: [number, number, number][] | null; segmentBoundaries: number[]; surfaces: string[]; highways: string[]; maxspeeds: string[]; smoothnesses: string[]; tracktypes: string[]; cycleways: string[]; bikeroutes: string[]; colorMode: ColorMode; } export function useWaypointManager( yjs: YjsState, poiState: ReturnType, suppressMapClickRef: React.RefObject, onRouteRequest?: (waypoints: WaypointData[]) => void, ) { const [state, setState] = useState({ waypoints: [], routeCoordinates: null, segmentBoundaries: [], surfaces: [], highways: [], maxspeeds: [], smoothnesses: [], tracktypes: [], cycleways: [], bikeroutes: [], colorMode: "plain", }); // Sync waypoints from Yjs useEffect(() => { const update = () => { const wps = extractWaypointData(yjs.waypoints); setState((prev) => ({ ...prev, waypoints: 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 { coordinates, ...computed } = readComputedRoute(yjs.routeData); setState((prev) => ({ ...prev, ...computed, routeCoordinates: coordinates, colorMode: getColorMode(yjs.routeData), })); }; yjs.routeData.observe(update); update(); return () => yjs.routeData.unobserve(update); }, [yjs.routeData]); const addWaypoint = useCallback( (lat: number, lng: number, name?: string) => { const { routeCoordinates, segmentBoundaries } = state; const snap = snapToPoi(lat, lng, poiState.pois); const finalLat = snap.lat; const finalLon = snap.snapped ? snap.lon : lng; let insertIndex = yjs.waypoints.length; if (routeCoordinates && routeCoordinates.length >= 2 && segmentBoundaries.length > 0) { let bestDist = Infinity; let bestSegment = -1; for (let seg = 0; seg < segmentBoundaries.length; seg++) { const start = segmentBoundaries[seg]!; const end = seg + 1 < segmentBoundaries.length ? segmentBoundaries[seg + 1]! : routeCoordinates.length; for (let i = start; i < end - 1; i++) { const c1 = routeCoordinates[i]!; const c2 = routeCoordinates[i + 1]!; const dist = pointToSegmentDist(finalLat, finalLon, c1[1]!, c1[0]!, c2[1]!, c2[0]!); if (dist < bestDist) { bestDist = dist; bestSegment = seg; } } } if (bestDist < 0.01 && bestSegment >= 0) { insertIndex = bestSegment + 1; } } yjs.doc.transact(() => { const yMap = new Y.Map(); yMap.set("lat", finalLat); yMap.set("lon", finalLon); if (snap.name) yMap.set("name", snap.name); else if (name) yMap.set("name", name); if (snap.osmId) yMap.set("osmId", snap.osmId); if (snap.poiTags) yMap.set("poiTags", snap.poiTags); yjs.waypoints.insert(insertIndex, [yMap]); }, "local"); }, [yjs.doc, yjs.waypoints, poiState.pois, state], ); const insertWaypointAtSegment = useCallback( (segmentIndex: number, lat: number, lon: number) => { const snap = snapToPoi(lat, lon, poiState.pois); yjs.doc.transact(() => { const yMap = new Y.Map(); yMap.set("lat", snap.lat); yMap.set("lon", snap.snapped ? snap.lon : lon); if (snap.name) yMap.set("name", snap.name); if (snap.osmId) yMap.set("osmId", snap.osmId); if (snap.poiTags) yMap.set("poiTags", snap.poiTags); yjs.waypoints.insert(segmentIndex + 1, [yMap]); }, "local"); }, [yjs.doc, yjs.waypoints, poiState.pois], ); const handleRouteInsert = useCallback( (pointIndex: number, lat: number, lon: number) => { suppressMapClickRef.current = true; const segIdx = findSegmentForPoint(pointIndex, state.segmentBoundaries); insertWaypointAtSegment(segIdx, lat, lon); }, [state.segmentBoundaries, insertWaypointAtSegment, suppressMapClickRef], ); const moveWaypoint = useCallback( (index: number, lat: number, lng: number) => { const snap = snapToPoi(lat, lng, poiState.pois); const yMap = yjs.waypoints.get(index); if (yMap) { yjs.doc.transact(() => { yMap.set("lat", snap.lat); yMap.set("lon", snap.snapped ? snap.lon : lng); if (snap.snapped && snap.name) { yMap.set("name", snap.name); } else { yMap.delete("name"); } if (snap.osmId) { yMap.set("osmId", snap.osmId); if (snap.poiTags) yMap.set("poiTags", snap.poiTags); } else { yMap.delete("osmId"); yMap.delete("poiTags"); } }, "local"); } }, [yjs.waypoints, yjs.doc, poiState.pois], ); const deleteWaypoint = useCallback( (index: number) => { yjs.doc.transact(() => { yjs.waypoints.delete(index, 1); }, "local"); }, [yjs.waypoints], ); const handleRoutePolylineHover = useCallback( (e: { latlng: { lat: number; lng: number } }, onRouteHover: (d: number | null) => void) => { const { routeCoordinates } = state; if (!routeCoordinates || routeCoordinates.length < 2) return; const { lat, lng } = e.latlng; let bestIdx = 0; let bestDist = Infinity; for (let i = 0; i < routeCoordinates.length; i++) { const c = routeCoordinates[i]!; const dLat = c[1]! - lat; const dLon = c[0]! - lng; const dist = dLat * dLat + dLon * dLon; if (dist < bestDist) { bestDist = dist; bestIdx = i; } } let totalDist = 0; for (let i = 1; i <= bestIdx; i++) { const prev = routeCoordinates[i - 1]!; const curr = routeCoordinates[i]!; const R = 6371000; const toRad = (d: number) => (d * Math.PI) / 180; const dLat = toRad(curr[1]! - prev[1]!); const dLon = toRad(curr[0]! - prev[0]!); const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(prev[1]!)) * Math.cos(toRad(curr[1]!)) * Math.sin(dLon / 2) ** 2; totalDist += R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } onRouteHover(totalDist); }, [state], ); return { ...state, addWaypoint, insertWaypointAtSegment, handleRouteInsert, moveWaypoint, deleteWaypoint, handleRoutePolylineHover, }; }