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, 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 | null>(null); const [renderedNonAis, setRenderedNonAis] = useState([]); useEffect(() => { const renderer = new ShipBatchRenderer(); 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 | null>(null); const [renderedAis, setRenderedAis] = useState([]); useEffect(() => { const renderer = new ShipBatchRenderer(); 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(), []); 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]); }