import { useEffect, useRef } from 'react'; import { MapContainer, TileLayer, Marker, Tooltip, useMap } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; import { isValidLocation } from '../utils/pathUtils'; import type { ResolvedPath, SenderInfo } from '../utils/pathUtils'; interface PathRouteMapProps { resolved: ResolvedPath; senderInfo: SenderInfo; } // Colors for hop markers (indexed by hop number - 1) const HOP_COLORS = [ '#f97316', // Hop 1: orange '#eab308', // Hop 2: yellow '#22c55e', // Hop 3: green '#06b6d4', // Hop 4: cyan '#ec4899', // Hop 5: pink '#f43f5e', // Hop 6: rose '#a855f7', // Hop 7: purple '#64748b', // Hop 8: slate ]; const SENDER_COLOR = '#3b82f6'; // blue const RECEIVER_COLOR = '#8b5cf6'; // violet function makeIcon(label: string, color: string): L.DivIcon { return L.divIcon({ className: '', iconSize: [24, 24], iconAnchor: [12, 12], html: `
${label}
`, }); } function getHopColor(hopIndex: number): string { return HOP_COLORS[hopIndex % HOP_COLORS.length]; } /** Collect all valid [lat, lon] points for bounds fitting */ function collectPoints(resolved: ResolvedPath): [number, number][] { const pts: [number, number][] = []; if (isValidLocation(resolved.sender.lat, resolved.sender.lon)) { pts.push([resolved.sender.lat!, resolved.sender.lon!]); } for (const hop of resolved.hops) { for (const m of hop.matches) { if (isValidLocation(m.lat, m.lon)) { pts.push([m.lat!, m.lon!]); } } } if (isValidLocation(resolved.receiver.lat, resolved.receiver.lon)) { pts.push([resolved.receiver.lat!, resolved.receiver.lon!]); } return pts; } /** Fit map bounds once on mount, then let the user pan/zoom freely */ function RouteMapBounds({ points }: { points: [number, number][] }) { const map = useMap(); const fitted = useRef(false); useEffect(() => { if (fitted.current || points.length === 0) return; fitted.current = true; if (points.length === 1) { map.setView(points[0], 12); } else { map.fitBounds(points as L.LatLngBoundsExpression, { padding: [30, 30], maxZoom: 14 }); } }, [map, points]); return null; } export function PathRouteMap({ resolved, senderInfo }: PathRouteMapProps) { const points = collectPoints(resolved); const hasAnyGps = points.length > 0; // Check if some nodes are missing GPS let totalNodes = 2; // sender + receiver let nodesWithGps = 0; if (isValidLocation(resolved.sender.lat, resolved.sender.lon)) nodesWithGps++; if (isValidLocation(resolved.receiver.lat, resolved.receiver.lon)) nodesWithGps++; for (const hop of resolved.hops) { if (hop.matches.length === 0) { totalNodes++; } else { totalNodes += hop.matches.length; nodesWithGps += hop.matches.filter((m) => isValidLocation(m.lat, m.lon)).length; } } const someMissingGps = hasAnyGps && nodesWithGps < totalNodes; if (!hasAnyGps) { return (
No nodes in this route have GPS coordinates
); } const center: [number, number] = points[0]; return (
{/* Sender marker */} {isValidLocation(resolved.sender.lat, resolved.sender.lon) && ( {senderInfo.name || 'Sender'} )} {/* Hop markers */} {resolved.hops.map((hop, hopIdx) => hop.matches .filter((m) => isValidLocation(m.lat, m.lon)) .map((m, mIdx) => ( {m.name || m.public_key.slice(0, 12)} )) )} {/* Receiver marker */} {isValidLocation(resolved.receiver.lat, resolved.receiver.lon) && ( {resolved.receiver.name || 'Receiver'} )}
{someMissingGps && (

Some nodes in this route have no GPS and are not shown

)}
); }