/* global React, L */ const { useEffect: useEffectMap, useRef: useRefMap, useState: useStateMap } = React; function TrajectoryMap({ trajectory, fromAirport, toAirport, t }) { const tx = t || ((k) => k); const mapEl = useRefMap(null); const mapRef = useRefMap(null); const layerRef = useRefMap(null); const natLayerRef = useRefMap(null); const [showNat, setShowNat] = useStateMap(false); const [natTracks, setNatTracks] = useStateMap([]); // init map once useEffectMap(() => { if (mapRef.current || !mapEl.current || typeof L === "undefined") return; const map = L.map(mapEl.current, { center: [30, 10], zoom: 2, zoomControl: true, worldCopyJump: true, attributionControl: true, scrollWheelZoom: true, minZoom: 2, }); L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", { attribution: "© OpenStreetMap · CARTO", subdomains: "abcd", maxZoom: 18, }).addTo(map); mapRef.current = map; setTimeout(() => map.invalidateSize(), 50); const ro = new ResizeObserver(() => map.invalidateSize()); ro.observe(mapEl.current); return () => ro.disconnect(); }, []); // fetch NAT tracks once on mount useEffectMap(() => { fetch("/nat-tracks?ocean=atlantic") .then((r) => r.ok ? r.json() : { tracks: [] }) .then((data) => { const tracks = Array.isArray(data) ? data : (data.tracks || []); if (tracks.length) setNatTracks(tracks); }) .catch(() => {}); }, []); // draw / hide NAT layer when showNat or natTracks changes useEffectMap(() => { const map = mapRef.current; if (!map) return; if (natLayerRef.current) { map.removeLayer(natLayerRef.current); natLayerRef.current = null; } if (!showNat || !natTracks.length) return; const group = L.layerGroup(); natTracks.forEach((track) => { if (!track.coords || track.coords.length < 2) return; L.polyline(track.coords, { color: "#1a6fbd", weight: 1.5, opacity: 0.55, dashArray: "6 8", }).bindTooltip(track.name || "NAT", { className: "penumbra-tip", sticky: true }) .addTo(group); // direction arrow at midpoint const mid = track.coords[Math.floor(track.coords.length / 2)]; if (mid) { const icon = L.divIcon({ className: "", html: `
${track.name || ""}
`, iconAnchor: [20, 10], }); L.marker(mid, { icon, interactive: false }).addTo(group); } }); group.addTo(map); natLayerRef.current = group; }, [showNat, natTracks]); // redraw trajectory whenever it changes useEffectMap(() => { const map = mapRef.current; if (!map) return; if (layerRef.current) { map.removeLayer(layerRef.current); layerRef.current = null; } if (!trajectory) return; const group = L.layerGroup(); // dashed path connecting samples const latlngs = trajectory.samples.map(s => [s.lat, s.lon]); L.polyline(latlngs, { color: "#0a0a0a", weight: 1.2, opacity: 0.45, dashArray: "3 5", }).addTo(group); // colored dots trajectory.samples.forEach((s) => { const icon = L.divIcon({ className: "", html: `
`, iconSize: [12, 12], iconAnchor: [6, 6], }); const m = L.marker([s.lat, s.lon], { icon, interactive: true }).addTo(group); const tip = `
${formatLatLon(s.lat, s.lon)}
${tx("tip_alt")}${Math.round(s.altFt).toLocaleString("fr-FR")} ft
${tx("tip_dose")}${s.doseRate.toFixed(2)} µSv/h
`; m.bindTooltip(tip, { className: "penumbra-tip", direction: "top", offset: [0, -6], sticky: false, }); }); // airport pins if (fromAirport) { const fromIcon = L.divIcon({ className: "", html: `
${fromAirport.iata}
`, iconSize: [24, 24], iconAnchor: [12, 12], }); L.marker([fromAirport.lat, fromAirport.lon], { icon: fromIcon, zIndexOffset: 1000 }) .addTo(group) .bindTooltip(`${fromAirport.city} · ${fromAirport.name}`, { className: "penumbra-tip", direction: "top" }); } if (toAirport) { const toIcon = L.divIcon({ className: "", html: `
${toAirport.iata}
`, iconSize: [24, 24], iconAnchor: [12, 12], }); L.marker([toAirport.lat, toAirport.lon], { icon: toIcon, zIndexOffset: 1000 }) .addTo(group) .bindTooltip(`${toAirport.city} · ${toAirport.name}`, { className: "penumbra-tip", direction: "top" }); } group.addTo(map); layerRef.current = group; // fit bounds const bounds = L.latLngBounds(latlngs); map.fitBounds(bounds, { padding: [60, 60] }); }, [trajectory, fromAirport, toAirport]); return ( <>
{!trajectory && (
{tx("map_empty")}
)}
{natTracks.length > 0 && ( )} {trajectory && (
{tx("legend_low")} {tx("legend_mod")} {tx("legend_high")}
)}
); } function formatLatLon(lat, lon) { const ns = lat >= 0 ? "N" : "S"; const ew = lon >= 0 ? "E" : "O"; return `${Math.abs(lat).toFixed(2)}°${ns} · ${Math.abs(lon).toFixed(2)}°${ew}`; } window.TrajectoryMap = TrajectoryMap;