import { useState, useEffect, useCallback, useRef } from 'react';
import { MapContainer, TileLayer, CircleMarker, Popup, Polyline, useMap } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import { api } from '../api/client';
import MapFilterPanel from '../components/MapFilterPanel';
import L from 'leaflet';
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
});
function MapUpdater({ center, zoom }) {
const map = useMap();
useEffect(() => {
if (center) map.flyTo(center, zoom, { duration: 1.2 });
}, [center, zoom, map]);
return null;
}
const RISK_COLORS = {
critical: '#f43f5e',
high: '#f59e0b',
medium: '#60a5fa',
low: '#10b981',
};
function getRiskColor(score) {
if (score >= 80) return RISK_COLORS.critical;
if (score >= 60) return RISK_COLORS.high;
if (score >= 40) return RISK_COLORS.medium;
return RISK_COLORS.low;
}
function getRiskLabel(score) {
if (score >= 80) return 'CRITICAL';
if (score >= 60) return 'HIGH';
if (score >= 40) return 'MEDIUM';
return 'LOW';
}
const REGION_CENTERS = {
all: { center: [28, 90], zoom: 4 },
china: { center: [35.86, 104.2], zoom: 4 },
india: { center: [22, 78.9], zoom: 5 },
};
// ─── Stat Card ───────────────────────────────────────────────────────────
function StatCard({ icon, value, label, color }) {
return (
);
}
// ─── Province Detail Sidebar ─────────────────────────────────────────────
function ProvinceDetail({ detail, onClose }) {
if (!detail) return null;
const color = getRiskColor(detail.risk_score);
const label = getRiskLabel(detail.risk_score);
const isIndia = detail.region === 'india';
return (
{isIndia ? '🇮🇳' : '🇨🇳'}
{detail.name}
{label} — {detail.risk_score.toFixed(1)}
{/* Risk Gauge */}
{detail.description && (
{detail.description}
)}
{/* Quick Stats */}
{detail.factories?.length > 0 && (
Factories
{detail.factories.length}
)}
Shocks
0 ? '#f43f5e' : 'var(--text)', marginTop: 2 }}>
{detail.shocks?.length || 0}
{/* Active Shocks */}
{detail.shocks?.length > 0 && (
⚠ Active Shocks
{detail.shocks.slice(0, 4).map(s => (
{(s.event_type || 'alert').replace(/_/g, ' ').toUpperCase()}
{s.title}
))}
)}
{/* Top Entities */}
{detail.top_entities?.length > 0 && (
{isIndia ? 'Top Consumed Drugs' : 'Dependent Products'}
{detail.top_entities.map(e => (
{e.name}
))}
)}
{/* Factories List */}
{detail.factories?.length > 0 && (
Key Facilities
{detail.factories.map((f, i) => (
{f}
))}
)}
);
}
// ─── Map Legend ───────────────────────────────────────────────────────────
function MapLegend() {
const items = [
{ color: RISK_COLORS.critical, label: 'Critical (80+)' },
{ color: RISK_COLORS.high, label: 'High (60-79)' },
{ color: RISK_COLORS.medium, label: 'Medium (40-59)' },
{ color: RISK_COLORS.low, label: 'Low (<40)' },
];
return (
Risk Level
{items.map(it => (
{it.label}
))}
Supply Corridor
);
}
// ─── Main Map Component ──────────────────────────────────────────────────
export default function MapView() {
const [heatmap, setHeatmap] = useState({ points: [] });
const [corridors, setCorridors] = useState({ corridors: [] });
const [stats, setStats] = useState(null);
const [filters, setFilters] = useState({ sector: 'both', risk_min: 0, shock_type: '', region: 'all' });
const [selectedProvince, setSelectedProvince] = useState(null);
const [provinceDetail, setProvinceDetail] = useState(null);
const [showCorridors, setShowCorridors] = useState(false);
const [loading, setLoading] = useState(true);
const debounceRef = useRef(null);
const regionConf = REGION_CENTERS[filters.region] || REGION_CENTERS.all;
const fetchMapData = useCallback(async (f) => {
setLoading(true);
try {
const [hm, cr, st] = await Promise.all([
api.getMapHeatmap(f),
api.getMapCorridors(f.sector),
api.getMapStats(),
]);
setHeatmap(hm);
setCorridors(cr);
setStats(st);
} catch (err) {
console.error('Map data fetch failed', err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => fetchMapData(filters), 300);
return () => clearTimeout(debounceRef.current);
}, [filters, fetchMapData]);
useEffect(() => {
if (selectedProvince) {
api.getMapProvince(selectedProvince, filters.sector)
.then(setProvinceDetail)
.catch(console.error);
} else {
setProvinceDetail(null);
}
}, [selectedProvince, filters.sector]);
const chinaPoints = (heatmap.points || []).filter(p => p.region === 'china');
const indiaPoints = (heatmap.points || []).filter(p => p.region === 'india');
const totalPoints = heatmap.points?.length || 0;
return (
{/* ── Left Sidebar ────────────────────────────────────────────── */}
{/* Title */}
Real-time geospatial intelligence across {totalPoints} monitored nodes.
{/* Quick Stats */}
{stats && (
)}
{/* Filter Panel */}
setShowCorridors(v => !v)}
/>
{/* Province Detail */}
setSelectedProvince(null)} />
{/* ── Map Container ───────────────────────────────────────────── */}
{/* Loading Indicator */}
{loading && (
Updating map data…
)}
{/* Supply Corridors */}
{showCorridors && corridors.corridors?.map(c => {
const riskColor = getRiskColor(c.risk_score || 30);
return (
🇨🇳 {c.source.name} ➜ 🇮🇳 {c.target.name}
{c.weight} dependent product{c.weight > 1 ? 's' : ''} on this route
{c.affected_entities?.length > 0 && (
{c.affected_entities.map(e => (
{e}
))}
)}
);
})}
{/* China Province Markers */}
{chinaPoints.map(p => {
const color = getRiskColor(p.risk_score);
const radius = Math.max(8, Math.min(28, p.risk_score / 3.5));
const isShocked = p.shock_count > 0;
return (
setSelectedProvince(p.id) }}
>
🇨🇳
{p.name}
Risk:
{p.risk_score.toFixed(1)}
{isShocked && (
⚠ {p.shock_count} Active Shock{p.shock_count > 1 ? 's' : ''}
)}
);
})}
{/* India State Markers */}
{indiaPoints.map(p => {
const color = getRiskColor(p.risk_score);
const radius = Math.max(7, Math.min(22, p.risk_score / 4));
return (
setSelectedProvince(p.id) }}
>
🇮🇳
{p.name}
Dependency Risk:
{p.risk_score.toFixed(1)}
{p.shock_count > 0 && (
📡 {p.shock_count} upstream shock{p.shock_count > 1 ? 's' : ''}
)}
);
})}
{/* Legend */}
{/* Map CSS Overrides */}
);
}