import { useRef, useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Map, NavigationControl, Marker, Source, Layer } from 'react-map-gl/maplibre'; import type { MapRef } from 'react-map-gl/maplibre'; import { ShipLayer } from '../layers/ShipLayer'; import { InfraLayer } from './InfraLayer'; import { SatelliteLayer } from '../layers/SatelliteLayer'; import { AircraftLayer } from '../layers/AircraftLayer'; import { SubmarineCableLayer } from './SubmarineCableLayer'; import { CctvLayer } from './CctvLayer'; import { KoreaAirportLayer } from './KoreaAirportLayer'; import { CoastGuardLayer } from './CoastGuardLayer'; import { NavWarningLayer } from './NavWarningLayer'; import { OsintMapLayer } from './OsintMapLayer'; import { EezLayer } from './EezLayer'; import { PiracyLayer } from './PiracyLayer'; import { WindFarmLayer } from './WindFarmLayer'; import { PortLayer } from './PortLayer'; import { MilitaryBaseLayer } from './MilitaryBaseLayer'; import { GovBuildingLayer } from './GovBuildingLayer'; import { NKLaunchLayer } from './NKLaunchLayer'; import { NKMissileEventLayer } from './NKMissileEventLayer'; import { ChineseFishingOverlay } from './ChineseFishingOverlay'; import { AnalysisOverlay } from './AnalysisOverlay'; import { FleetClusterLayer } from './FleetClusterLayer'; import { FishingZoneLayer } from './FishingZoneLayer'; import { AnalysisStatsPanel } from './AnalysisStatsPanel'; import { getMarineTrafficCategory } from '../../utils/marineTraffic'; import { classifyFishingZone } from '../../utils/fishingAnalysis'; import { fetchKoreaInfra } from '../../services/infra'; import type { PowerFacility } from '../../services/infra'; import type { Ship, Aircraft, SatellitePosition } from '../../types'; import type { OsintItem } from '../../services/osint'; import type { UseVesselAnalysisResult } from '../../hooks/useVesselAnalysis'; import { countryLabelsGeoJSON } from '../../data/countryLabels'; import 'maplibre-gl/dist/maplibre-gl.css'; export interface KoreaFiltersState { illegalFishing: boolean; illegalTransship: boolean; darkVessel: boolean; cableWatch: boolean; dokdoWatch: boolean; ferryWatch: boolean; } interface Props { ships: Ship[]; allShips?: Ship[]; aircraft: Aircraft[]; satellites: SatellitePosition[]; layers: Record; osintFeed: OsintItem[]; currentTime: number; koreaFilters: KoreaFiltersState; transshipSuspects: Set; cableWatchSuspects: Set; dokdoWatchSuspects: Set; dokdoAlerts: { mmsi: string; name: string; dist: number; time: number }[]; vesselAnalysis?: UseVesselAnalysisResult; } // MarineTraffic-style: satellite + dark ocean + nautical overlay const MAP_STYLE = { version: 8 as const, sources: { 'satellite': { type: 'raster' as const, tiles: [ 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', ], tileSize: 256, maxzoom: 19, attribution: '© Esri, Maxar', }, 'carto-dark': { type: 'raster' as const, tiles: [ 'https://a.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', 'https://b.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', 'https://c.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', ], tileSize: 256, }, 'opensea': { type: 'raster' as const, tiles: [ 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', ], tileSize: 256, maxzoom: 18, }, }, layers: [ { id: 'background', type: 'background' as const, paint: { 'background-color': '#0d1f3c' } }, { id: 'satellite-base', type: 'raster' as const, source: 'satellite', paint: { 'raster-brightness-max': 0.75, 'raster-saturation': -0.1, 'raster-contrast': 0.05 } }, { id: 'dark-overlay', type: 'raster' as const, source: 'carto-dark', paint: { 'raster-opacity': 0.25 } }, { id: 'seamark', type: 'raster' as const, source: 'opensea', paint: { 'raster-opacity': 0.5 } }, ], }; // Korea-centered view const KOREA_MAP_CENTER = { longitude: 127.5, latitude: 36 }; const KOREA_MAP_ZOOM = 6; const FILTER_ICON: Record = { illegalFishing: '\u{1F6AB}\u{1F41F}', illegalTransship: '\u2693', darkVessel: '\u{1F47B}', cableWatch: '\u{1F50C}', dokdoWatch: '\u{1F3DD}\uFE0F', ferryWatch: '\u{1F6A2}', }; const FILTER_COLOR: Record = { illegalFishing: '#ef4444', illegalTransship: '#f97316', darkVessel: '#8b5cf6', cableWatch: '#00e5ff', dokdoWatch: '#22c55e', ferryWatch: '#2196f3', }; const FILTER_I18N_KEY: Record = { illegalFishing: 'filters.illegalFishingMonitor', illegalTransship: 'filters.illegalTransshipMonitor', darkVessel: 'filters.darkVesselMonitor', cableWatch: 'filters.cableWatchMonitor', dokdoWatch: 'filters.dokdoWatchMonitor', ferryWatch: 'filters.ferryWatchMonitor', }; export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintFeed, currentTime, koreaFilters, transshipSuspects, cableWatchSuspects, dokdoWatchSuspects, dokdoAlerts, vesselAnalysis }: Props) { const { t } = useTranslation(); const mapRef = useRef(null); const [infra, setInfra] = useState([]); const [flyToTarget, setFlyToTarget] = useState<{ lng: number; lat: number; zoom: number } | null>(null); const [selectedAnalysisMmsi, setSelectedAnalysisMmsi] = useState(null); const [trackCoords, setTrackCoords] = useState<[number, number][] | null>(null); useEffect(() => { fetchKoreaInfra().then(setInfra).catch(() => {}); }, []); useEffect(() => { if (flyToTarget && mapRef.current) { mapRef.current.flyTo({ center: [flyToTarget.lng, flyToTarget.lat], zoom: flyToTarget.zoom, duration: 1500 }); setFlyToTarget(null); } }, [flyToTarget]); useEffect(() => { if (!selectedAnalysisMmsi) setTrackCoords(null); }, [selectedAnalysisMmsi]); const handleAnalysisShipSelect = useCallback((mmsi: string) => { setSelectedAnalysisMmsi(mmsi); const ship = (allShips ?? ships).find(s => s.mmsi === mmsi); if (ship) setFlyToTarget({ lng: ship.lng, lat: ship.lat, zoom: 12 }); }, [allShips, ships]); const handleTrackLoad = useCallback((_mmsi: string, coords: [number, number][]) => { setTrackCoords(coords); }, []); const handleFleetZoom = useCallback((bounds: { minLat: number; maxLat: number; minLng: number; maxLng: number }) => { mapRef.current?.fitBounds( [[bounds.minLng, bounds.minLat], [bounds.maxLng, bounds.maxLat]], { padding: 60, duration: 1500 }, ); }, []); return ( {layers.ships && } {/* Illegal fishing vessel markers — allShips(라이브 위치) 기반 */} {koreaFilters.illegalFishing && (allShips ?? ships).filter(s => { const mtCat = getMarineTrafficCategory(s.typecode, s.category); if (mtCat !== 'fishing' || s.flag === 'KR') return false; return classifyFishingZone(s.lat, s.lng).zone !== 'OUTSIDE'; }).slice(0, 200).map(s => (
{/* 강조 펄스 링 — 선박 아이콘 중앙에 오버레이 */}
{/* 선박명 — 아이콘 아래 */}
{s.name || s.mmsi}
))} {/* Transship suspect labels — Marker DOM, inline styles kept for dynamic border color */} {transshipSuspects.size > 0 && ships.filter(s => transshipSuspects.has(s.mmsi)).map(s => (
{`\u26A0 ${t('korea.transshipSuspect')}`}
))} {/* Cable watch suspect labels */} {cableWatchSuspects.size > 0 && ships.filter(s => cableWatchSuspects.has(s.mmsi)).map(s => (
{`\u{1F50C} ${t('korea.cableDanger')}`}
))} {/* Dokdo watch labels (Japanese vessels) */} {dokdoWatchSuspects.size > 0 && ships.filter(s => dokdoWatchSuspects.has(s.mmsi)).map(s => { const dist = Math.round(Math.hypot( (s.lng - 131.8647) * Math.cos((37.2417 * Math.PI) / 180), s.lat - 37.2417, ) * 111); const inTerritorial = dist < 22; return (
{inTerritorial ? '\u{1F6A8}' : '\u26A0'} {'\u{1F1EF}\u{1F1F5}'} {inTerritorial ? t('korea.dokdoIntrusion') : t('korea.dokdoApproach')} {`${dist}km`}
); })} {layers.infra && infra.length > 0 && } {layers.satellites && satellites.length > 0 && } {layers.aircraft && aircraft.length > 0 && } {layers.cables && } {layers.cctv && } {layers.windFarm && } {layers.ports && } {layers.militaryBases && } {layers.govBuildings && } {layers.nkLaunch && } {layers.nkMissile && } {koreaFilters.illegalFishing && } {layers.cnFishing && } {layers.cnFishing && vesselAnalysis && vesselAnalysis.clusters.size > 0 && ( )} {vesselAnalysis && vesselAnalysis.analysisMap.size > 0 && ( )} {layers.airports && } {layers.coastGuard && } {layers.navWarning && } {layers.osint && } {layers.eez && } {layers.piracy && } {/* Filter Status Banner */} {(() => { const active = (Object.keys(koreaFilters) as (keyof KoreaFiltersState)[]).filter(k => koreaFilters[k]); if (active.length === 0) return null; return (
{active.map(k => { const color = FILTER_COLOR[k]; return (
{FILTER_ICON[k]} {t(FILTER_I18N_KEY[k])}
); })}
{t('korea.detected', { count: ships.length })}
); })()} {/* Dokdo alert panel */} {dokdoAlerts.length > 0 && koreaFilters.dokdoWatch && (
{`\u{1F6A8} ${t('korea.dokdoAlerts')}`}
{dokdoAlerts.map((a, i) => (
{a.dist < 22 ? `\u{1F6A8} ${t('korea.territorialIntrusion')}` : `\u26A0 ${t('korea.approachWarning')}`} {new Date(a.time).toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
{`\u{1F1EF}\u{1F1F5} ${a.name} \u2014 ${t('korea.dokdoDistance', { dist: a.dist })}`}
))}
)} {/* 선택된 분석 선박 항적 — tracks API 응답 기반 */} {trackCoords && trackCoords.length > 1 && ( )} {/* AI Analysis Stats Panel — 항상 표시 */} {vesselAnalysis && ( )} ); }