import { useMemo, useState, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Map, Marker, Popup, Source, Layer, NavigationControl } from 'react-map-gl/maplibre'; import type { MapRef } from 'react-map-gl/maplibre'; import { AircraftLayer } from '../layers/AircraftLayer'; import { SatelliteLayer } from '../layers/SatelliteLayer'; import { ShipLayer } from '../layers/ShipLayer'; import { DamagedShipLayer } from '../layers/DamagedShipLayer'; import { SeismicMarker } from '../layers/SeismicMarker'; import { OilFacilityLayer } from './OilFacilityLayer'; import { AirportLayer } from './AirportLayer'; import { MEFacilityLayer } from './MEFacilityLayer'; import { iranOilFacilities } from '../../data/oilFacilities'; import { middleEastAirports } from '../../data/airports'; import type { GeoEvent, Aircraft, SatellitePosition, Ship, LayerVisibility } from '../../types'; import { countryLabelsGeoJSON } from '../../data/countryLabels'; import maplibregl from 'maplibre-gl'; import 'maplibre-gl/dist/maplibre-gl.css'; interface Props { events: GeoEvent[]; currentTime: number; aircraft: Aircraft[]; satellites: SatellitePosition[]; ships: Ship[]; layers: LayerVisibility; hoveredShipMmsi?: string | null; focusShipMmsi?: string | null; onFocusShipClear?: () => void; flyToTarget?: { lat: number; lng: number; zoom?: number } | null; onFlyToDone?: () => void; seismicMarker?: { lat: number; lng: number; magnitude: number; place: string } | null; } // ESRI World Imagery + ESRI boundaries overlay const SATELLITE_STYLE = { version: 8 as const, sources: { 'esri-satellite': { type: 'raster' as const, tiles: [ 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', ], tileSize: 256, attribution: '© Esri, Maxar, Earthstar Geographics', maxzoom: 19, }, 'esri-boundaries': { type: 'raster' as const, tiles: [ 'https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}', ], tileSize: 256, maxzoom: 19, }, }, layers: [ { id: 'background', type: 'background' as const, paint: { 'background-color': '#000811' } }, { id: 'satellite', type: 'raster' as const, source: 'esri-satellite' }, { id: 'boundaries', type: 'raster' as const, source: 'esri-boundaries', paint: { 'raster-opacity': 0.65 } }, ], }; const EVENT_COLORS: Record = { airstrike: '#ef4444', explosion: '#f97316', missile_launch: '#eab308', intercept: '#3b82f6', alert: '#a855f7', impact: '#ff0000', osint: '#06b6d4', }; const SOURCE_COLORS: Record = { US: '#ef4444', IL: '#22c55e', IR: '#ff0000', proxy: '#f59e0b', }; function getEventColor(event: GeoEvent): string { if (event.type === 'impact') return '#ff0000'; if (event.source && SOURCE_COLORS[event.source]) return SOURCE_COLORS[event.source]; return EVENT_COLORS[event.type]; } const EVENT_RADIUS: Record = { airstrike: 12, explosion: 10, missile_launch: 8, intercept: 7, alert: 6, impact: 14, osint: 8, }; export function SatelliteMap({ events, currentTime, aircraft, satellites, ships, layers, hoveredShipMmsi, focusShipMmsi, onFocusShipClear, flyToTarget, onFlyToDone, seismicMarker }: Props) { const { t } = useTranslation(); const mapRef = useRef(null); const [selectedEventId, setSelectedEventId] = useState(null); useEffect(() => { if (flyToTarget && mapRef.current) { mapRef.current.flyTo({ center: [flyToTarget.lng, flyToTarget.lat], zoom: flyToTarget.zoom ?? 8, duration: 1200, }); onFlyToDone?.(); } }, [flyToTarget, onFlyToDone]); const visibleEvents = useMemo(() => { if (!layers.events) return []; return events.filter(e => e.timestamp <= currentTime); }, [events, currentTime, layers.events]); const selectedEvent = useMemo( () => visibleEvents.find(e => e.id === selectedEventId) || null, [visibleEvents, selectedEventId], ); const countryLabels = useMemo(() => countryLabelsGeoJSON(), []); return ( {/* Korean country labels */} {/* Event markers */} {visibleEvents.map(ev => ( { e.originalEvent.stopPropagation(); setSelectedEventId(ev.id); }} >
))} {/* Popup */} {selectedEvent && ( setSelectedEventId(null)} closeOnClick={false} maxWidth="320px" className="event-popup" >
{selectedEvent.source && ( {t(`source.${selectedEvent.source}`)} )} {selectedEvent.type === 'impact' && (
{t('popup.impactSite')}
)}
{selectedEvent.label}
{new Date(selectedEvent.timestamp + 9 * 3600_000).toISOString().slice(0, 16).replace('T', ' ')} KST {selectedEvent.description && (

{selectedEvent.description}

)} {selectedEvent.imageUrl && (
{selectedEvent.imageCaption { (e.target as HTMLImageElement).style.display = 'none'; }} /> {selectedEvent.imageCaption && (
{selectedEvent.imageCaption}
)}
)}
{selectedEvent.lat.toFixed(4)}°N, {selectedEvent.lng.toFixed(4)}°E
)} {/* Overlay layers */} {layers.aircraft && } {layers.satellites && } {layers.ships && } {seismicMarker && } {layers.oilFacilities && } {layers.airports && } {layers.meFacilities && } ); }