import { useEffect, useMemo } from 'react' import { MapContainer, TileLayer, CircleMarker, Polyline, Popup, Tooltip, useMap } from 'react-leaflet' import type { LatLngBoundsExpression, LatLngTuple } from 'leaflet' import 'leaflet/dist/leaflet.css' import type { NodeInfo, EdgeInfo } from '@/lib/api' import { ExternalLink, MapPin } from 'lucide-react' // Fix Leaflet default marker icon issue with Vite import L from 'leaflet' import markerIcon from 'leaflet/dist/images/marker-icon.png' import markerIcon2x from 'leaflet/dist/images/marker-icon-2x.png' import markerShadow from 'leaflet/dist/images/marker-shadow.png' // @ts-expect-error - Leaflet icon fix delete L.Icon.Default.prototype._getIconUrl L.Icon.Default.mergeOptions({ iconUrl: markerIcon, iconRetinaUrl: markerIcon2x, shadowUrl: markerShadow, }) interface GeoMapProps { nodes: NodeInfo[] edges: EdgeInfo[] selectedNodeId: number | null onSelectNode: (nodeId: number | null) => void } const REGION_COLORS = ['#3b82f6', '#a78bfa', '#06b6d4', '#f59e0b', '#22c55e', '#ec4899', '#8b5cf6', '#14b8a6'] const INFRA_ROLES = ['ROUTER', 'ROUTER_LATE', 'REPEATER', 'TRACKER'] function getQualityColor(snr: number): string { if (snr > 12) return '#22c55e' if (snr > 8) return '#4ade80' if (snr > 5) return '#f59e0b' if (snr > 3) return '#f97316' return '#ef4444' } function getRegionIndex(lat: number | null): number { if (lat === null) return 0 if (lat > 46) return 0 if (lat > 44.5) return 1 if (lat > 43) return 2 return 3 } function formatLastHeard(lastHeard: string | null): string { if (!lastHeard) return 'Unknown' const date = new Date(lastHeard) const now = new Date() const diffMs = now.getTime() - date.getTime() const diffMins = Math.floor(diffMs / 60000) const diffHours = Math.floor(diffMs / 3600000) const diffDays = Math.floor(diffMs / 86400000) if (diffMins < 1) return 'Just now' if (diffMins < 60) return `${diffMins}m ago` if (diffHours < 24) return `${diffHours}h ago` return `${diffDays}d ago` } // Component to fit bounds on mount function FitBounds({ bounds }: { bounds: LatLngBoundsExpression | null }) { const map = useMap() useEffect(() => { if (bounds) { map.fitBounds(bounds, { padding: [50, 50] }) } }, [map, bounds]) return null } interface NodePopupProps { node: NodeInfo } function NodePopup({ node }: NodePopupProps) { const hasCoords = node.latitude !== null && node.longitude !== null const batteryText = node.battery_level !== null ? (node.battery_level > 100 || (node.voltage && node.voltage > 4.1) ? 'USB ⚡' : `${node.battery_level.toFixed(0)}%`) : 'Unknown' return (