import { useEffect, useState, useCallback, useRef } from "react";
import { MapContainer, TileLayer, LayersControl, Marker, useMapEvents, useMap } from "react-leaflet";
import L from "leaflet";
import * as Y from "yjs";
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 { parseGpxAsync, extractWaypoints } from "@trails-cool/gpx";
import { isOvernight } from "~/lib/overnight";
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 { snapToPoi } from "~/lib/poi-snap";
import { Z_CURSOR, Z_WAYPOINT, Z_WAYPOINT_HIGHLIGHTED, Z_HIGHLIGHT } from "~/lib/z-index";
import { NoGoAreaLayer } from "./NoGoAreaLayer";
import { ColoredRoute, findSegmentForPoint, type ColorMode } from "./ColoredRoute";
import { RouteInteraction } from "./RouteInteraction";
import { PoiPanel, PoiMarkers } from "./PoiPanel";
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 WaypointData {
lat: number;
lon: number;
name?: string;
overnight: boolean;
}
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,
overnight: isOvernight(yMap),
}));
}
interface PlannerMapProps {
yjs: YjsState;
onRouteRequest?: (waypoints: WaypointData[]) => void;
onImportError?: (message: string) => void;
highlightPosition?: [number, number] | null;
highlightedWaypoint?: number | null;
days?: DayStage[];
}
function MapExposer() {
const map = useMap();
useEffect(() => {
if (typeof window !== "undefined") {
(window as unknown as Record).__leafletMap = map;
}
}, [map]);
return null;
}
function RouteFitter({ coordinates }: { coordinates: [number, number, number][] | null }) {
const map = useMap();
const hasFitted = useRef(false);
useEffect(() => {
if (hasFitted.current || !coordinates || coordinates.length < 2) return;
// Coordinates are in [lon, lat, elevation] GeoJSON format
const bounds = L.latLngBounds(
coordinates.map((c) => [c[1]!, c[0]!] as [number, number]),
);
if (!bounds.isValid()) return;
// Delay fitBounds so the layout has settled (elevation chart may resize the map)
const raf = requestAnimationFrame(() => {
map.invalidateSize();
map.fitBounds(bounds, { padding: [50, 50], maxZoom: 16 });
hasFitted.current = true;
});
return () => cancelAnimationFrame(raf);
}, [coordinates, 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