diff --git a/src/map/MapContainer.jsx b/src/map/MapContainer.jsx index 969c7440..0a4a2bf8 100644 --- a/src/map/MapContainer.jsx +++ b/src/map/MapContainer.jsx @@ -7,7 +7,7 @@ import { defaults as defaultInteractions, DragBox } from 'ol/interaction'; import { platformModifierKeyOnly } from 'ol/events/condition'; import { createBaseLayers } from './layers/baseLayer'; -import { useMapStore } from '../stores/mapStore'; +import { useMapStore, BASE_MAP_TYPES } from '../stores/mapStore'; import useShipStore from '../stores/shipStore'; import useShipData from '../hooks/useShipData'; import useShipLayer from '../hooks/useShipLayer'; @@ -15,8 +15,20 @@ import ShipLegend from '../components/ship/ShipLegend'; import ShipTooltip from '../components/ship/ShipTooltip'; import ShipDetailModal from '../components/ship/ShipDetailModal'; import ShipContextMenu from '../components/ship/ShipContextMenu'; +import TopBar from '../components/map/TopBar'; +import GlobalTrackQueryViewer from '../tracking/components/GlobalTrackQueryViewer'; +import useReplayLayer from '../replay/hooks/useReplayLayer'; +import { shipBatchRenderer } from './ShipBatchRenderer'; +import useReplayStore from '../replay/stores/replayStore'; +import useAnimationStore from '../replay/stores/animationStore'; +import ReplayTimeline from '../replay/components/ReplayTimeline'; +import { unregisterReplayLayers } from '../replay/utils/replayLayerRegistry'; +import { showLiveShips } from '../utils/liveControl'; +import { useTrackQueryStore } from '../tracking/stores/trackQueryStore'; +import { LAYER_IDS as TRACK_QUERY_LAYER_IDS } from '../tracking/utils/trackQueryLayerUtils'; import useMeasure from './measure/useMeasure'; +import useTrackingMode from '../hooks/useTrackingMode'; import './measure/measure.scss'; import './MapContainer.scss'; @@ -33,10 +45,13 @@ const HOVER_THROTTLE_MS = 50; export default function MapContainer() { const mapRef = useRef(null); const mapInstanceRef = useRef(null); - const { map, setMap, setZoom, center } = useMapStore(); - const { showLegend } = useShipStore(); + const baseLayersRef = useRef(null); // 배경지도 레이어 참조 + const { map, setMap, setZoom, center, baseMapType } = useMapStore(); + const showLegend = useShipStore((s) => s.showLegend); const hoverInfo = useShipStore((s) => s.hoverInfo); const detailModals = useShipStore((s) => s.detailModals); + const replayCompleted = useReplayStore((s) => s.queryCompleted); + const replayQuery = useReplayStore((s) => s.currentQuery); // STOMP 선박 데이터 연결 useShipData({ autoConnect: true }); @@ -44,20 +59,35 @@ export default function MapContainer() { // Deck.gl 선박 레이어 const { deckRef } = useShipLayer(map); + // 리플레이 레이어 + useReplayLayer(); + // 측정 도구 useMeasure(); + // 추적 모드 (함정 중심 지도 이동 + 반경 원) + useTrackingMode(); + + // 배경지도 타입 변경 시 레이어 가시성 토글 + useEffect(() => { + if (!baseLayersRef.current) return; + + const { worldMap, encMap, darkMap } = baseLayersRef.current; + + worldMap.setVisible(baseMapType === BASE_MAP_TYPES.NORMAL); + encMap.setVisible(baseMapType === BASE_MAP_TYPES.ENC); + darkMap.setVisible(baseMapType === BASE_MAP_TYPES.DARK); + }, [baseMapType]); + // 호버 쓰로틀 타이머 const hoverTimerRef = useRef(null); /** - * deck.pickObject 헬퍼 + * deck.pickObject 헬퍼 (라이브 선박 전용) */ const pickShip = useCallback((pixel) => { const deck = deckRef.current; if (!deck) return null; - - // deck.layerManager가 초기화되기 전에 pickObject 호출하면 assertion error 발생 if (!deck.layerManager) return null; try { @@ -72,6 +102,26 @@ export default function MapContainer() { } }, [deckRef]); + /** + * deck.pickObject 헬퍼 (모든 레이어) + */ + const pickAny = useCallback((pixel) => { + const deck = deckRef.current; + if (!deck) return null; + if (!deck.layerManager) return null; + + try { + // layerIds를 지정하지 않으면 모든 pickable 레이어에서 픽킹 + const result = deck.pickObject({ + x: pixel[0], + y: pixel[1], + }); + return result || null; + } catch { + return null; + } + }, [deckRef]); + /** * OpenLayers pointermove → 호버 툴팁 */ @@ -79,6 +129,8 @@ export default function MapContainer() { // 드래그 중이면 무시 if (evt.dragging) { useShipStore.getState().setHoverInfo(null); + useTrackQueryStore.getState().setHighlightedVesselId(null); + useReplayStore.getState().setHighlightedVesselId(null); return; } @@ -89,17 +141,107 @@ export default function MapContainer() { hoverTimerRef.current = null; const pixel = evt.pixel; - const ship = pickShip(pixel); + const { clientX, clientY } = evt.originalEvent; + const pickResult = pickAny(pixel); - if (ship) { - // evt.originalEvent에서 화면 좌표 가져오기 - const { clientX, clientY } = evt.originalEvent; - useShipStore.getState().setHoverInfo({ ship, x: clientX, y: clientY }); - } else { + if (!pickResult || !pickResult.layer) { + // 아무것도 픽킹되지 않음 useShipStore.getState().setHoverInfo(null); + useTrackQueryStore.getState().setHighlightedVesselId(null); + useTrackQueryStore.getState().clearHoveredPoint(); + useReplayStore.getState().setHighlightedVesselId(null); + return; } + + const layerId = pickResult.layer.id; + const obj = pickResult.object; + + // 라이브 선박 + if (layerId === 'ship-icon-layer') { + useShipStore.getState().setHoverInfo({ ship: obj, x: clientX, y: clientY }); + useTrackQueryStore.getState().setHighlightedVesselId(null); + useTrackQueryStore.getState().clearHoveredPoint(); + useReplayStore.getState().setHighlightedVesselId(null); + return; + } + + // 항적조회 경로 (PathLayer) + if (layerId === TRACK_QUERY_LAYER_IDS.PATH) { + useShipStore.getState().setHoverInfo(null); + useTrackQueryStore.getState().setHighlightedVesselId(obj?.vesselId || null); + useTrackQueryStore.getState().clearHoveredPoint(); + useReplayStore.getState().setHighlightedVesselId(null); + return; + } + + // 항적조회 포인트 (ScatterplotLayer) + if (layerId === TRACK_QUERY_LAYER_IDS.POINTS) { + useShipStore.getState().setHoverInfo(null); + useTrackQueryStore.getState().setHighlightedVesselId(obj?.vesselId || null); + useReplayStore.getState().setHighlightedVesselId(null); + // 포인트 호버 정보 설정 + if (obj) { + useTrackQueryStore.getState().setHoveredPoint({ + vesselId: obj.vesselId, + position: obj.position, + timestamp: obj.timestamp, + speed: obj.speed, + index: obj.index, + }, clientX, clientY); + } else { + useTrackQueryStore.getState().clearHoveredPoint(); + } + return; + } + + // 항적조회 가상 선박 아이콘 + if (layerId === TRACK_QUERY_LAYER_IDS.VIRTUAL_SHIP) { + const tooltipShip = { + shipName: obj.shipName, + targetId: obj.vesselId?.split('_').pop() || obj.vesselId, + signalKindCode: obj.shipKindCode, + sog: obj.speed || 0, + cog: obj.heading || 0, + }; + useShipStore.getState().setHoverInfo({ ship: tooltipShip, x: clientX, y: clientY }); + useTrackQueryStore.getState().setHighlightedVesselId(obj?.vesselId || null); + useTrackQueryStore.getState().clearHoveredPoint(); + useReplayStore.getState().setHighlightedVesselId(null); + return; + } + + // 리플레이 경로 (PathLayer) + if (layerId === 'track-path-layer') { + useShipStore.getState().setHoverInfo(null); + useTrackQueryStore.getState().setHighlightedVesselId(null); + useTrackQueryStore.getState().clearHoveredPoint(); + useReplayStore.getState().setHighlightedVesselId(obj?.vesselId || null); + return; + } + + // 리플레이 가상 선박 아이콘 + if (layerId === 'track-virtual-ship-layer') { + const tooltipShip = { + shipName: obj.shipName, + targetId: obj.vesselId?.split('_').pop() || obj.vesselId, + signalKindCode: obj.shipKindCode, + sog: obj.speed || 0, + cog: obj.heading || 0, + }; + useShipStore.getState().setHoverInfo({ ship: tooltipShip, x: clientX, y: clientY }); + useTrackQueryStore.getState().setHighlightedVesselId(null); + useTrackQueryStore.getState().clearHoveredPoint(); + useReplayStore.getState().setHighlightedVesselId(obj?.vesselId || null); + return; + } + + // 기타: 모두 클리어 + useShipStore.getState().setHoverInfo(null); + useTrackQueryStore.getState().setHighlightedVesselId(null); + useTrackQueryStore.getState().clearHoveredPoint(); + useReplayStore.getState().setHighlightedVesselId(null); }, HOVER_THROTTLE_MS); - }, [pickShip]); + }, [pickAny]); /** * OpenLayers dblclick → 상세 모달 @@ -115,10 +257,13 @@ export default function MapContainer() { }, [pickShip]); /** - * pointerout → 툴팁 숨김 + * pointerout → 툴팁 숨김 + 하이라이트 클리어 */ const handlePointerOut = useCallback(() => { useShipStore.getState().setHoverInfo(null); + useTrackQueryStore.getState().setHighlightedVesselId(null); + useTrackQueryStore.getState().clearHoveredPoint(); + useReplayStore.getState().setHighlightedVesselId(null); }, []); /** @@ -177,8 +322,14 @@ export default function MapContainer() { useEffect(() => { if (!mapRef.current || mapInstanceRef.current) return; - // 베이스 레이어 생성 - const { worldMap, eastAsiaMap, korMap } = createBaseLayers(); + // 현재 배경지도 타입 가져오기 + const currentBaseMapType = useMapStore.getState().baseMapType; + + // 베이스 레이어 생성 (3가지 배경지도 + 상세지도) + const { worldMap, encMap, darkMap, eastAsiaMap, korMap } = createBaseLayers(currentBaseMapType); + + // 배경지도 레이어 참조 저장 + baseLayersRef.current = { worldMap, encMap, darkMap }; // 스케일라인 컨트롤 (해리 단위) const scaleLineControl = new ScaleLine({ @@ -192,14 +343,16 @@ export default function MapContainer() { target: mapRef.current, layers: [ worldMap, + encMap, + darkMap, eastAsiaMap, korMap, ], view: new View({ center: fromLonLat(center), zoom: 7, - minZoom: 0, - maxZoom: 17, + minZoom: 5, // 야간지도 타일 최소 레벨 + maxZoom: 15, // 줌 확장은 15까지 (타일은 12레벨까지만 로드) }), controls: defaultControls({ attribution: false, @@ -270,6 +423,8 @@ export default function MapContainer() { // 스토어에 맵 인스턴스 저장 setMap(map); mapInstanceRef.current = map; + // TrackQueryViewer 등에서 줌 감지용 + window.__mainMap__ = map; // 클린업 return () => { @@ -277,12 +432,14 @@ export default function MapContainer() { mapInstanceRef.current.setTarget(null); mapInstanceRef.current = null; } + window.__mainMap__ = null; }; }, []); return ( <>
+