import { useState, useCallback, useRef } from "react";
import { MapContainer, TileLayer, LayersControl, Marker, Polyline } 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";
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 "leaflet/dist/leaflet.css";
function waypointIcon(index: number, overnight?: boolean, highlighted?: boolean): L.DivIcon {
const bg = overnight ? "#8B6D3A" : "#2563eb";
const scale = highlighted ? "scale(1.17)" : "scale(1)";
return L.divIcon({
className: "",
html: `
${overnight ? "☾" : index + 1}
`,
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;
onRouteHover?: (distance: number | null) => void;
days?: DayStage[];
}
export function PlannerMap({ yjs, sessionId, onRouteRequest, highlightPosition, highlightedWaypoint, 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 handleRoutePolylineOut = useCallback(() => {
onRouteHover?.(null);
}, [onRouteHover]);
return (
{draggingOver && (
)}
{baseLayers.map((layer) => (
))}
{overlayLayers.map((layer) => (
))}
{} : addWaypoint} suppressRef={suppressMapClickRef} />
{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 as unknown as Event);
const orig = e.originalEvent as MouseEvent;
setContextMenu({ x: orig.clientX, y: orig.clientY, index: i });
},
}}
/>
))}
{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)}
/>
)}
);
}