WING-GIS 해양경찰 통합 GIS 위치정보시스템. 모노레포: frontend(React 19 + MapLibre + deck.gl) + services(Spring Boot + Gradle). - npm + Nexus 프록시 레지스트리 설정 - 팀 워크플로우 v1.6.1 부트스트랩 파일 배치 - .githooks (commit-msg, post-checkout) - custom_pre_commit: true (모노레포 pre-commit 별도 관리) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
130 lines
4.3 KiB
TypeScript
130 lines
4.3 KiB
TypeScript
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
|
import type { LayersList } from '@deck.gl/core';
|
|
import type maplibregl from 'maplibre-gl';
|
|
import { useStore } from '../../../hooks/useStore';
|
|
import { toVesselFeatures, type VesselFeature } from '../lib/vesselAdapter';
|
|
import { buildVesselDeckLayers } from '../lib/vesselDeckLayers';
|
|
import { toAisFeatures, aisTargetToVessel, type AisFeature } from '../lib/aisAdapter';
|
|
import { buildAisDeckLayers } from '../lib/aisDeckLayers';
|
|
import { ShipBatchRenderer, type ViewportBounds } from '../lib/ShipBatchRenderer';
|
|
|
|
export function useVesselDeckLayer(
|
|
mapRef: React.MutableRefObject<maplibregl.Map | null>,
|
|
mapSyncEpoch: number,
|
|
): LayersList {
|
|
const vessels = useStore((s) => s.vessels);
|
|
const aisTargets = useStore((s) => s.aisTargets);
|
|
const signalState = useStore((s) => s.signalState);
|
|
const selectedVessel = useStore((s) => s.selectedVessel);
|
|
const setSelectedVessel = useStore((s) => s.setSelectedVessel);
|
|
|
|
// Non-AIS dummy vessel renderer
|
|
const nonAisRendererRef = useRef<ShipBatchRenderer<VesselFeature> | null>(null);
|
|
const [renderedNonAis, setRenderedNonAis] = useState<VesselFeature[]>([]);
|
|
|
|
useEffect(() => {
|
|
const renderer = new ShipBatchRenderer<VesselFeature>();
|
|
renderer.initialize((ships) => setRenderedNonAis(ships));
|
|
nonAisRendererRef.current = renderer;
|
|
return () => { renderer.dispose(); nonAisRendererRef.current = null; };
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const nonAis = vessels.filter((v) => v.source !== 'AIS');
|
|
nonAisRendererRef.current?.setData(toVesselFeatures(nonAis));
|
|
}, [vessels]);
|
|
|
|
// AIS renderer
|
|
const aisRendererRef = useRef<ShipBatchRenderer<AisFeature> | null>(null);
|
|
const [renderedAis, setRenderedAis] = useState<AisFeature[]>([]);
|
|
|
|
useEffect(() => {
|
|
const renderer = new ShipBatchRenderer<AisFeature>();
|
|
renderer.initialize((ships) => setRenderedAis(ships));
|
|
aisRendererRef.current = renderer;
|
|
return () => { renderer.dispose(); aisRendererRef.current = null; };
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const features = toAisFeatures(aisTargets);
|
|
const renderer = aisRendererRef.current;
|
|
if (renderer) {
|
|
renderer.setData(features);
|
|
} else {
|
|
// Renderer not ready yet, set directly
|
|
setRenderedAis(features);
|
|
}
|
|
}, [aisTargets]);
|
|
|
|
// Viewport sync for both renderers
|
|
useEffect(() => {
|
|
const map = mapRef.current;
|
|
if (!map || mapSyncEpoch === 0) return;
|
|
|
|
const updateViewport = () => {
|
|
const bounds = map.getBounds();
|
|
const vp: ViewportBounds = {
|
|
minLon: bounds.getWest(),
|
|
maxLon: bounds.getEast(),
|
|
minLat: bounds.getSouth(),
|
|
maxLat: bounds.getNorth(),
|
|
};
|
|
const zoom = map.getZoom();
|
|
|
|
for (const r of [nonAisRendererRef.current, aisRendererRef.current]) {
|
|
if (!r) continue;
|
|
r.setViewportBounds(vp);
|
|
r.setZoom(zoom);
|
|
r.requestRender();
|
|
}
|
|
};
|
|
|
|
map.on('moveend', updateViewport);
|
|
updateViewport();
|
|
return () => { map.off('moveend', updateViewport); };
|
|
}, [mapRef, mapSyncEpoch]);
|
|
|
|
// Click handlers
|
|
const handleNonAisClick = useCallback(
|
|
(info: { object?: VesselFeature }) => {
|
|
if (info.object) setSelectedVessel(info.object.raw);
|
|
},
|
|
[setSelectedVessel],
|
|
);
|
|
|
|
const handleAisClick = useCallback(
|
|
(info: { object?: AisFeature }) => {
|
|
if (info.object) setSelectedVessel(aisTargetToVessel(info.object.raw));
|
|
},
|
|
[setSelectedVessel],
|
|
);
|
|
|
|
const selectedMmsi = selectedVessel?.mmsi ?? null;
|
|
const highlightedMmsis = useMemo(() => new Set<string>(), []);
|
|
|
|
const nonAisLayers = useMemo(
|
|
() => buildVesselDeckLayers({
|
|
ships: renderedNonAis,
|
|
signalState,
|
|
selectedMmsi,
|
|
highlightedMmsis,
|
|
onClick: handleNonAisClick,
|
|
}),
|
|
[renderedNonAis, signalState, selectedMmsi, highlightedMmsis, handleNonAisClick],
|
|
);
|
|
|
|
const selectedMmsiNum = selectedMmsi ? Number(selectedMmsi) : null;
|
|
|
|
const aisLayers = useMemo(
|
|
() => buildAisDeckLayers({
|
|
ships: renderedAis,
|
|
aisVisible: signalState['AIS'] !== false,
|
|
selectedMmsi: Number.isFinite(selectedMmsiNum) ? selectedMmsiNum : null,
|
|
onClick: handleAisClick,
|
|
}),
|
|
[renderedAis, signalState, selectedMmsiNum, handleAisClick],
|
|
);
|
|
|
|
return useMemo(() => [...aisLayers, ...nonAisLayers], [aisLayers, nonAisLayers]);
|
|
}
|