import { useState, useCallback, useRef } from "react";
import { MapContainer, TileLayer, LayersControl, Marker, Polyline, Tooltip } from "react-leaflet";
import L from "leaflet";
import { useTranslation } from "react-i18next";
import type { DayStage } from "@trails-cool/gpx";
import type { YjsState } from "~/lib/use-yjs";
import { baseLayers, overlayLayers } from "@trails-cool/map-core";
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 { Z_WAYPOINT, Z_WAYPOINT_HIGHLIGHTED, Z_HIGHLIGHT } from "@trails-cool/map-core";
import { NoGoAreaLayer } from "./NoGoAreaLayer";
import { ColoredRoute } from "./ColoredRoute";
import { RouteInteraction } from "./RouteInteraction";
import { PoiPanel, PoiMarkers } from "./PoiPanel";
import { WaypointContextMenu } from "./WaypointContextMenu";
import {
MapExposer,
RouteFitter,
MapClickHandler,
CursorTracker,
NoGoAreaButton,
OverlaySync,
PoiRefresher,
} from "./MapHelpers";
import { useWaypointManager } from "~/lib/use-waypoint-manager";
import { useGpxDrop } from "~/lib/use-gpx-drop";
import { useNearbyPois } from "~/lib/use-nearby-pois";
import { NearbyPoiMarkers } from "./NearbyPoiMarkers";
import { poiCategories } from "@trails-cool/map-core";
import type { Poi } from "~/lib/pois";
import "leaflet/dist/leaflet.css";
function waypointIcon(index: number, overnight?: boolean, highlighted?: boolean, hasNote?: boolean): L.DivIcon {
const bg = overnight ? "#8B6D3A" : "#2563eb";
const scale = highlighted ? "scale(1.17)" : "scale(1)";
const noteIndicator = hasNote
? `✎`
: "";
return L.divIcon({
className: "",
html: `
${overnight ? "☾" : index + 1}
${noteIndicator}
`,
iconSize: [0, 0],
});
}
function dayLabelIcon(dayNumber: number, distanceKm: string): L.DivIcon {
return L.divIcon({
className: "",
html: `
Day ${dayNumber} · ${distanceKm} km
`,
iconSize: [0, 0],
});
}
interface PlannerMapProps {
yjs: YjsState;
sessionId: string;
onRouteRequest?: (waypoints: { lat: number; lon: number; name?: string; overnight: boolean }[]) => void;
onImportError?: (message: string) => void;
highlightPosition?: [number, number] | null;
highlightedWaypoint?: number | null;
selectedWaypointIndex?: number | null;
onRouteHover?: (distance: number | null) => void;
days?: DayStage[];
}
export function PlannerMap({ yjs, sessionId, onRouteRequest, highlightPosition, highlightedWaypoint, selectedWaypointIndex, onRouteHover, onImportError, days }: PlannerMapProps) {
const { t } = useTranslation("planner");
const poiState = usePois(sessionId);
useProfileDefaults(yjs, poiState);
useYjsPoiSync(yjs, poiState);
const [enabledOverlays, setEnabledOverlays] = useState([]);
const [selectedBaseLayer, setSelectedBaseLayer] = useState(baseLayers[0]!.name);
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; index: number } | null>(null);
const [noGoDrawing, setNoGoDrawing] = useState(false);
const toggleNoGoDraw = useCallback(() => setNoGoDrawing((v) => !v), []);
const suppressMapClickRef = useRef(false);
const routeInteractionSuspendedRef = useRef(false);
const waypointDraggingRef = useRef(false);
const {
waypoints,
routeCoordinates,
segmentBoundaries,
surfaces,
highways,
maxspeeds,
smoothnesses,
tracktypes,
cycleways,
bikeroutes,
colorMode,
addWaypoint,
handleRouteInsert,
moveWaypoint,
deleteWaypoint,
handleRoutePolylineHover,
} = useWaypointManager(yjs, poiState, suppressMapClickRef, onRouteRequest);
const { draggingOver, handleDragEnter, handleDragLeave, handleDragOver, handleDrop } = useGpxDrop(yjs, onImportError);
const selectedWp = selectedWaypointIndex != null ? waypoints[selectedWaypointIndex] : undefined;
const nearbyPoisState = useNearbyPois(selectedWp?.lat, selectedWp?.lon);
const handleSnapToPoi = useCallback((poi: Poi) => {
if (selectedWaypointIndex == null) return;
const yMap = yjs.waypoints.get(selectedWaypointIndex);
if (!yMap) return;
const cat = poiCategories.find((c) => c.id === poi.category);
const notePrefix = cat ? `${cat.icon} ${poi.name ?? cat.id}` : (poi.name ?? "");
yjs.doc.transact(() => {
yMap.set("lat", poi.lat);
yMap.set("lon", poi.lon);
if (poi.name && !yMap.get("name")) yMap.set("name", poi.name);
const existing = yMap.get("note") as string | undefined;
yMap.set("note", existing ? `${notePrefix}\n${existing}` : notePrefix);
if (poi.id) yMap.set("osmId", poi.id);
}, "local");
}, [selectedWaypointIndex, yjs, poiCategories]);
const handleRoutePolylineOut = useCallback(() => {
onRouteHover?.(null);
}, [onRouteHover]);
return (
{draggingOver && (
)}
{baseLayers.map((layer) => (
))}
{overlayLayers.map((layer) => (
))}
{} : addWaypoint} suppressRef={suppressMapClickRef} />
{selectedWaypointIndex != null && nearbyPoisState.pois.length > 0 && (
)}
{waypoints.map((wp, i) => (
{ routeInteractionSuspendedRef.current = true; },
mouseout: () => {
if (!waypointDraggingRef.current) routeInteractionSuspendedRef.current = false;
},
dragstart: () => {
waypointDraggingRef.current = true;
yjs.undoManager.stopCapturing();
routeInteractionSuspendedRef.current = true;
},
dragend: (e) => {
waypointDraggingRef.current = false;
routeInteractionSuspendedRef.current = false;
const { lat, lng } = e.target.getLatLng();
moveWaypoint(i, lat, lng);
},
contextmenu: (e) => {
L.DomEvent.preventDefault(e.originalEvent);
const orig = e.originalEvent as MouseEvent;
setContextMenu({ x: orig.clientX, y: orig.clientY, index: i });
},
}}
>
{wp.note && (
{wp.note.length > 120 ? wp.note.slice(0, 120) + "…" : wp.note}
)}
))}
{days && days.length > 1 && days.map((day) => {
const wp = waypoints[day.endWaypointIndex];
if (!wp || day.dayNumber === days.length) return null;
return (
);
})}
{routeCoordinates && routeCoordinates.length >= 2 && (
<>
{onRouteHover && (
[c[1]!, c[0]!] as [number, number])}
pathOptions={{ weight: 20, opacity: 0, interactive: true }}
eventHandlers={{
mouseover: (e) => handleRoutePolylineHover(e, onRouteHover),
mousemove: (e) => handleRoutePolylineHover(e, onRouteHover),
mouseout: handleRoutePolylineOut,
}}
/>
)}
>
)}
{highlightPosition && (
',
iconSize: [0, 0],
})}
/>
)}
{contextMenu && (
deleteWaypoint(contextMenu.index)}
onToggleOvernight={() => setOvernight(yjs, contextMenu.index, !waypoints[contextMenu.index]?.overnight)}
onClose={() => setContextMenu(null)}
/>
)}
);
}