feat: MapContainer 통합 (항적조회, 리플레이, 추적 모드)
- TopBar, 항적조회 뷰어, 리플레이 타임라인 통합 - 배경지도 전환 레이어 가시성 토글 - 추적 모드 훅 연동 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
부모
83f5f72b0e
커밋
f273080d5e
@ -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 (
|
||||
<>
|
||||
<div id="map" ref={mapRef} className="map-container" />
|
||||
<TopBar />
|
||||
{showLegend && <ShipLegend />}
|
||||
{hoverInfo && (
|
||||
<ShipTooltip ship={hoverInfo.ship} x={hoverInfo.x} y={hoverInfo.y} />
|
||||
@ -291,6 +448,20 @@ export default function MapContainer() {
|
||||
<ShipDetailModal key={modal.id} modal={modal} />
|
||||
))}
|
||||
<ShipContextMenu />
|
||||
<GlobalTrackQueryViewer />
|
||||
{replayCompleted && (
|
||||
<ReplayTimeline
|
||||
fromDate={replayQuery?.startTime}
|
||||
toDate={replayQuery?.endTime}
|
||||
onClose={() => {
|
||||
useReplayStore.getState().reset();
|
||||
useAnimationStore.getState().reset();
|
||||
unregisterReplayLayers();
|
||||
showLiveShips(); // 라이브 선박 다시 표시
|
||||
shipBatchRenderer.immediateRender();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user