import { useState, useEffect, useRef, useCallback } from 'react' const API = import.meta.env.VITE_API_URL || '' import { MapContainer, TileLayer, Marker, Popup, CircleMarker, useMap } from 'react-leaflet' import L from 'leaflet' import 'leaflet/dist/leaflet.css' delete L.Icon.Default.prototype._getIconUrl L.Icon.Default.mergeOptions({ iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png', iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png', shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png', }) const CCY_POS = { USD:[38,-97], EUR:[51,10], GBP:[52.5,-1.5], JPY:[36,138], AUD:[-25,134], CAD:[56,-106], CHF:[46.8,8.2], NZD:[-41,174], CNY:[35,105], CNH:[22.3,114.2], SEK:[60,18], NOK:[60,8], DKK:[56,10], SGD:[1.35,103.8], HKD:[22.3,114.2], MXN:[23,-102], BRL:[-14,-51], ZAR:[-30,25], INR:[20,77], KRW:[36,128], } function planeIcon(heading) { const deg = (heading || 0) - 45 return L.divIcon({ html: ``, iconSize: [13,13], iconAnchor: [6,6], className:'', }) } function eventIcon(impact) { const c = impact === 'High' ? '#ff4c5e' : impact === 'Medium' ? '#f5a623' : '#3ddc84' return L.divIcon({ html: ``, iconSize: [8,8], iconAnchor: [4,4], className:'', }) } function quakeColor(m) { if (m >= 7) return '#ff0040' if (m >= 6) return '#ff4c5e' if (m >= 5.5) return '#f5a623' return '#f5a62355' } function quakeR(m) { return Math.max(5, (m - 4) * 7) } function RadarLayer({ active }) { const map = useMap() const ref = useRef(null) useEffect(() => { if (!active) { if (ref.current) { map.removeLayer(ref.current); ref.current = null } return } let cancelled = false fetch('https://api.rainviewer.com/public/weather-maps.json') .then(r => r.json()) .then(d => { if (cancelled) return const past = d.radar?.past || [] if (!past.length) return const t = past[past.length - 1].time if (ref.current) map.removeLayer(ref.current) ref.current = L.tileLayer( `${d.host}/v2/radar/${t}/512/{z}/{x}/{y}/4/1_1.png`, { opacity: 0.52, zIndex: 5, attribution: 'RainViewer' } ).addTo(map) }).catch(() => {}) return () => { cancelled = true if (ref.current) { map.removeLayer(ref.current); ref.current = null } } }, [active, map]) return null } const LAYER_CFG = [ { key:'aircraft', label:'AIRCRAFT', color:'var(--cyan)' }, { key:'quakes', label:'EARTHQUAKES', color:'var(--red)' }, { key:'weather', label:'RADAR', color:'var(--purple)'}, { key:'events', label:'FOREX EVENTS', color:'var(--amber)' }, ] export default function WorldMap() { const [layers, setLayers] = useState({ aircraft:true, quakes:true, weather:false, events:true }) const [aircraft,setAircraft]= useState([]) const [quakes, setQuakes] = useState([]) const [events, setEvents] = useState([]) const [stats, setStats] = useState({ planes:0, quakes:0, events:0 }) const [updated, setUpdated] = useState(null) const [loadingAc, setLoadingAc] = useState(false) const toggle = k => setLayers(p => ({ ...p, [k]: !p[k] })) const loadAircraft = useCallback(async () => { setLoadingAc(true) try { const r = await fetch('${API}/api/live/aircraft') const d = await r.json() const planes = (d.states || []) .filter(s => s[5] != null && s[6] != null && !s[8]) .slice(0, 500) .map(s => ({ id: s[0], call: (s[1]||'').trim() || s[0], country: s[2], lon: s[5], lat: s[6], alt: s[7] ? Math.round(s[7] / 0.3048).toLocaleString() : '?', spd: s[9] ? Math.round(s[9] * 1.944) : '?', hdg: s[10], })) setAircraft(planes) setStats(p => ({ ...p, planes: planes.length })) setUpdated(new Date().toLocaleTimeString()) } catch {} finally { setLoadingAc(false) } }, []) const loadQuakes = useCallback(async () => { try { const r = await fetch('${API}/api/live/earthquakes') const d = await r.json() const qs = d.features || [] setQuakes(qs) setStats(p => ({ ...p, quakes: qs.length })) } catch {} }, []) const loadEvents = useCallback(async () => { try { const r = await fetch('${API}/api/news/calendar') const d = await r.json() const evs = (d.events || []).filter(e => CCY_POS[e.country]) setEvents(evs) setStats(p => ({ ...p, events: evs.length })) } catch {} }, []) useEffect(() => { loadAircraft(); loadQuakes(); loadEvents() const iv = setInterval(loadAircraft, 30000) return () => clearInterval(iv) }, [loadAircraft, loadQuakes, loadEvents]) function refresh() { loadAircraft(); loadQuakes(); loadEvents() } return (