import { useRef, useEffect } from 'react'; import maplibregl from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; import { countryLabelsGeoJSON } from '../../data/countryLabels'; import type { GeoEvent, Aircraft, SatellitePosition, Ship, LayerVisibility } from '../../types'; interface Props { events: GeoEvent[]; currentTime: number; aircraft: Aircraft[]; satellites: SatellitePosition[]; ships: Ship[]; layers: LayerVisibility; } const EVENT_COLORS: Record = { airstrike: '#ef4444', explosion: '#f97316', missile_launch: '#eab308', intercept: '#3b82f6', alert: '#a855f7', impact: '#ff0000', osint: '#06b6d4', sea_attack: '#0ea5e9', }; // Navy flag-based colors for military vessels const NAVY_COLORS: Record = { US: '#1e90ff', UK: '#e63946', FR: '#ffd60a', KR: '#00e5ff', IR: '#2ecc40', JP: '#ff6b6b', AU: '#f4a261', }; const SHIP_COLORS: Record = { carrier: '#ef4444', destroyer: '#f97316', warship: '#fb923c', patrol: '#fbbf24', submarine: '#8b5cf6', tanker: '#22d3ee', cargo: '#94a3b8', civilian: '#64748b', }; const MIL_SHIP_CATS = ['carrier', 'destroyer', 'warship', 'submarine', 'patrol']; function getGlobeShipColor(cat: string, flag?: string): string { if (MIL_SHIP_CATS.includes(cat) && flag && NAVY_COLORS[flag]) return NAVY_COLORS[flag]; return SHIP_COLORS[cat] || '#64748b'; } const AC_COLORS: Record = { fighter: '#ef4444', bomber: '#dc2626', surveillance: '#f59e0b', tanker: '#22d3ee', transport: '#10b981', cargo: '#6366f1', helicopter: '#a855f7', civilian: '#64748b', unknown: '#475569', }; export function GlobeMap({ events, currentTime, aircraft, satellites, ships, layers }: Props) { const containerRef = useRef(null); const mapRef = useRef(null); const markersRef = useRef([]); // Initialize map useEffect(() => { if (!containerRef.current || mapRef.current) return; const map = new maplibregl.Map({ container: containerRef.current, style: { version: 8, sources: { 'dark-tiles': { type: 'raster', tiles: ['https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'], tileSize: 256, attribution: '© OpenStreetMap', }, }, layers: [ { id: 'background', type: 'background', paint: { 'background-color': '#0a0a1a' }, }, { id: 'dark-tiles', type: 'raster', source: 'dark-tiles', }, ], projection: { type: 'globe' }, } as maplibregl.StyleSpecification, center: [44, 31.5], zoom: 3, pitch: 20, }); map.addControl(new maplibregl.NavigationControl(), 'top-right'); map.on('load', () => { map.addSource('country-labels', { type: 'geojson', data: countryLabelsGeoJSON() }); map.addLayer({ id: 'country-label-lg', type: 'symbol', source: 'country-labels', filter: ['==', ['get', 'rank'], 1], layout: { 'text-field': ['get', 'name'], 'text-size': 14, 'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold'], 'text-allow-overlap': false, 'text-padding': 6, }, paint: { 'text-color': '#e2e8f0', 'text-halo-color': '#000', 'text-halo-width': 2, 'text-opacity': 0.9 }, }); map.addLayer({ id: 'country-label-md', type: 'symbol', source: 'country-labels', filter: ['==', ['get', 'rank'], 2], layout: { 'text-field': ['get', 'name'], 'text-size': 11, 'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold'], 'text-allow-overlap': false, 'text-padding': 4, }, paint: { 'text-color': '#94a3b8', 'text-halo-color': '#000', 'text-halo-width': 1.5, 'text-opacity': 0.85 }, }); map.addLayer({ id: 'country-label-sm', type: 'symbol', source: 'country-labels', filter: ['==', ['get', 'rank'], 3], minzoom: 5, layout: { 'text-field': ['get', 'name'], 'text-size': 10, 'text-font': ['Open Sans Regular', 'Arial Unicode MS Regular'], 'text-allow-overlap': false, 'text-padding': 2, }, paint: { 'text-color': '#64748b', 'text-halo-color': '#000', 'text-halo-width': 1, 'text-opacity': 0.75 }, }); }); mapRef.current = map; return () => { map.remove(); mapRef.current = null; }; }, []); // Update markers — DOM direct manipulation, inline styles intentionally kept useEffect(() => { const map = mapRef.current; if (!map) return; // Clear old markers for (const m of markersRef.current) m.remove(); markersRef.current = []; const addMarker = (lng: number, lat: number, color: string, size: number, tooltip: string) => { const el = document.createElement('div'); el.style.width = `${size}px`; el.style.height = `${size}px`; el.style.borderRadius = '50%'; el.style.background = color; el.style.border = `1.5px solid ${color}`; el.style.boxShadow = `0 0 ${size}px ${color}80`; el.style.cursor = 'pointer'; el.title = tooltip; const marker = new maplibregl.Marker({ element: el }) .setLngLat([lng, lat]) .addTo(map); markersRef.current.push(marker); }; const addTriangle = (lng: number, lat: number, color: string, size: number, heading: number, tooltip: string) => { const el = document.createElement('div'); el.style.width = `${size}px`; el.style.height = `${size}px`; el.style.transform = `rotate(${heading}deg)`; el.style.cursor = 'pointer'; el.title = tooltip; el.innerHTML = ` `; const marker = new maplibregl.Marker({ element: el }) .setLngLat([lng, lat]) .addTo(map); markersRef.current.push(marker); }; // Events if (layers.events) { const visible = events.filter(e => e.timestamp <= currentTime); for (const e of visible) { const color = EVENT_COLORS[e.type] || '#888'; const size = e.type === 'impact' ? 14 : 8; addMarker(e.lng, e.lat, color, size, `${e.label}\n${new Date(e.timestamp + 9 * 3600_000).toISOString().slice(0, 16).replace('T', ' ')} KST`); } } // Aircraft if (layers.aircraft) { const filtered = layers.militaryOnly ? aircraft.filter(a => a.category !== 'civilian' && a.category !== 'unknown') : aircraft; for (const ac of filtered) { const color = AC_COLORS[ac.category] || '#64748b'; addTriangle(ac.lng, ac.lat, color, 10, ac.heading || 0, `${ac.callsign || ac.icao24} [${ac.category}]\nAlt: ${ac.altitude?.toFixed(0) || '?'}ft`); } } // Satellites if (layers.satellites) { for (const sat of satellites) { addMarker(sat.lng, sat.lat, '#ef4444', 5, `${sat.name}\nAlt: ${sat.altitude?.toFixed(0)}km`); } } // Ships if (layers.ships) { const filtered = layers.militaryOnly ? ships.filter(s => !['civilian', 'cargo', 'tanker'].includes(s.category)) : ships; for (const s of filtered) { const color = getGlobeShipColor(s.category, s.flag); addTriangle(s.lng, s.lat, color, 10, s.heading || 0, `${s.name} [${s.category}]\n${s.flag || ''}`); } } }, [events, currentTime, aircraft, satellites, ships, layers]); return (
); }