import { useEffect, useMemo, type MutableRefObject } from 'react'; import { MapboxOverlay } from '@deck.gl/mapbox'; import { type PickingInfo } from '@deck.gl/core'; import type { AisTarget } from '../../../entities/aisTarget/model/types'; import type { LegacyVesselInfo } from '../../../entities/legacyVessel/model/types'; import type { FcLink, FleetCircle, PairLink } from '../../../features/legacyDashboard/model/types'; import type { MapToggleState } from '../../../features/mapToggles/MapToggles'; import type { DashSeg, Map3DSettings, MapProjectionId, PairRangeCircle } from '../types'; import { MaplibreDeckCustomLayer } from '../MaplibreDeckCustomLayer'; import { toSafeNumber } from '../lib/setUtils'; import { getShipTooltipHtml, getPairLinkTooltipHtml, getFcLinkTooltipHtml, getRangeTooltipHtml, getFleetCircleTooltipHtml, } from '../lib/tooltips'; import { sanitizeDeckLayerList } from '../lib/mapCore'; import { buildMercatorDeckLayers, buildGlobeDeckLayers } from '../lib/deckLayerFactories'; // NOTE: // Globe mode now relies on MapLibre native overlays (useGlobeOverlays/useGlobeShips). // Keep Deck custom-layer rendering disabled in globe to avoid projection-space artifacts. const ENABLE_GLOBE_DECK_OVERLAYS = false; export function useDeckLayers( mapRef: MutableRefObject, overlayRef: MutableRefObject, globeDeckLayerRef: MutableRefObject, projectionBusyRef: MutableRefObject, opts: { projection: MapProjectionId; settings: Map3DSettings; trackReplayDeckLayers: unknown[]; shipLayerData: AisTarget[]; shipOverlayLayerData: AisTarget[]; shipData: AisTarget[]; legacyHits: Map | null | undefined; pairLinks: PairLink[] | undefined; fcLinks: FcLink[] | undefined; fcDashed: DashSeg[]; fleetCircles: FleetCircle[] | undefined; pairRanges: PairRangeCircle[]; pairLinksInteractive: PairLink[]; pairRangesInteractive: PairRangeCircle[]; fcLinesInteractive: DashSeg[]; fleetCirclesInteractive: FleetCircle[]; overlays: MapToggleState; shipByMmsi: Map; selectedMmsi: number | null; shipHighlightSet: Set; isHighlightedFleet: (ownerKey: string, vesselMmsis: number[]) => boolean; isHighlightedPair: (aMmsi: number, bMmsi: number) => boolean; isHighlightedMmsi: (mmsi: number) => boolean; clearDeckHoverPairs: () => void; clearDeckHoverMmsi: () => void; clearMapFleetHoverState: () => void; setDeckHoverPairs: (next: number[]) => void; setDeckHoverMmsi: (next: number[]) => void; setMapFleetHoverState: (ownerKey: string | null, vesselMmsis: number[]) => void; toFleetMmsiList: (value: unknown) => number[]; touchDeckHoverState: (isHover: boolean) => void; hasAuxiliarySelectModifier: (ev?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null) => boolean; onDeckSelectOrHighlight: (info: unknown, allowMultiSelect?: boolean) => void; onSelectMmsi: (mmsi: number | null) => void; onToggleHighlightMmsi?: (mmsi: number) => void; ensureMercatorOverlay: () => MapboxOverlay | null; projectionRef: MutableRefObject; }, ) { const { projection, settings, trackReplayDeckLayers, shipLayerData, shipOverlayLayerData, shipData, legacyHits, pairLinks, fcDashed, fleetCircles, pairRanges, pairLinksInteractive, pairRangesInteractive, fcLinesInteractive, fleetCirclesInteractive, overlays, shipByMmsi, selectedMmsi, shipHighlightSet, isHighlightedFleet, isHighlightedPair, isHighlightedMmsi, clearDeckHoverPairs, clearDeckHoverMmsi, clearMapFleetHoverState, setDeckHoverPairs, setDeckHoverMmsi, setMapFleetHoverState, toFleetMmsiList, touchDeckHoverState, hasAuxiliarySelectModifier, onDeckSelectOrHighlight, onSelectMmsi, onToggleHighlightMmsi, ensureMercatorOverlay, projectionRef, } = opts; const legacyTargets = useMemo(() => { if (!legacyHits) return []; return shipData.filter((t) => legacyHits.has(t.mmsi)); }, [shipData, legacyHits]); const legacyTargetsOrdered = useMemo(() => { if (legacyTargets.length === 0) return legacyTargets; const layer = [...legacyTargets]; layer.sort((a, b) => a.mmsi - b.mmsi); return layer; }, [legacyTargets]); const legacyOverlayTargets = useMemo(() => { if (shipHighlightSet.size === 0) return []; return legacyTargets.filter((target) => shipHighlightSet.has(target.mmsi)); }, [legacyTargets, shipHighlightSet]); // Mercator Deck layers useEffect(() => { const map = mapRef.current; if (!map) return; if (projection !== 'mercator' || projectionBusyRef.current) { if (projection !== 'mercator') { try { if (overlayRef.current) overlayRef.current.setProps({ layers: [] } as never); } catch { // ignore } } return; } const deckTarget = ensureMercatorOverlay(); if (!deckTarget) return; const layers = buildMercatorDeckLayers({ shipLayerData, shipOverlayLayerData, legacyTargetsOrdered, legacyOverlayTargets, legacyHits, pairLinks, fcDashed, fleetCircles, pairRanges, pairLinksInteractive, pairRangesInteractive, fcLinesInteractive, fleetCirclesInteractive, overlays, showDensity: settings.showDensity, showShips: settings.showShips, selectedMmsi, shipHighlightSet, touchDeckHoverState, setDeckHoverPairs, setDeckHoverMmsi, clearDeckHoverPairs, clearMapFleetHoverState, setMapFleetHoverState, toFleetMmsiList, hasAuxiliarySelectModifier, onSelectMmsi, onToggleHighlightMmsi, onDeckSelectOrHighlight, }); const normalizedBaseLayers = sanitizeDeckLayerList(layers); const normalizedTrackLayers = sanitizeDeckLayerList(trackReplayDeckLayers); const normalizedLayers = sanitizeDeckLayerList([...normalizedBaseLayers, ...normalizedTrackLayers]); const deckProps = { layers: normalizedLayers, getTooltip: (info: PickingInfo) => { if (!info.object) return null; if (info.layer && info.layer.id === 'density') { // eslint-disable-next-line @typescript-eslint/no-explicit-any const o: any = info.object; const n = Array.isArray(o?.points) ? o.points.length : 0; return { text: `AIS density: ${n}` }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const obj: any = info.object; if (typeof obj.mmsi === 'number') { return getShipTooltipHtml({ mmsi: obj.mmsi, targetByMmsi: shipByMmsi, legacyHits }); } if (info.layer && info.layer.id === 'pair-lines') { const aMmsi = toSafeNumber(obj.aMmsi); const bMmsi = toSafeNumber(obj.bMmsi); if (aMmsi == null || bMmsi == null) return null; return getPairLinkTooltipHtml({ warn: !!obj.warn, distanceNm: toSafeNumber(obj.distanceNm), aMmsi, bMmsi, legacyHits, targetByMmsi: shipByMmsi }); } if (info.layer && info.layer.id === 'fc-lines') { const fcMmsi = toSafeNumber(obj.fcMmsi); const otherMmsi = toSafeNumber(obj.otherMmsi); if (fcMmsi == null || otherMmsi == null) return null; return getFcLinkTooltipHtml({ suspicious: !!obj.suspicious, distanceNm: toSafeNumber(obj.distanceNm), fcMmsi, otherMmsi, legacyHits, targetByMmsi: shipByMmsi }); } if (info.layer && info.layer.id === 'pair-range') { const aMmsi = toSafeNumber(obj.aMmsi); const bMmsi = toSafeNumber(obj.bMmsi); if (aMmsi == null || bMmsi == null) return null; return getRangeTooltipHtml({ warn: !!obj.warn, distanceNm: toSafeNumber(obj.distanceNm), aMmsi, bMmsi, legacyHits }); } if (info.layer && info.layer.id === 'fleet-circles') { return getFleetCircleTooltipHtml({ ownerKey: String(obj.ownerKey ?? ''), ownerLabel: String(obj.ownerKey ?? ''), count: Number(obj.count ?? 0) }); } return null; }, onClick: (info: PickingInfo) => { if (!info.object) { onSelectMmsi(null); return; } if (info.layer && info.layer.id === 'density') return; // eslint-disable-next-line @typescript-eslint/no-explicit-any const obj: any = info.object; if (typeof obj.mmsi === 'number') { const t = obj as AisTarget; const sourceEvent = (info as { srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null }).srcEvent; if (sourceEvent && hasAuxiliarySelectModifier(sourceEvent)) { onToggleHighlightMmsi?.(t.mmsi); return; } onSelectMmsi(t.mmsi); const clickOpts = { center: [t.lon, t.lat] as [number, number], zoom: Math.max(map.getZoom(), 10), duration: 600 }; if (projectionRef.current === 'globe') { map.flyTo(clickOpts); } else { map.easeTo(clickOpts); } } }, }; try { deckTarget.setProps(deckProps as never); } catch (e) { console.error('Failed to apply base mercator deck props. Keeping previous layer set.', e); } }, [ ensureMercatorOverlay, projection, shipLayerData, shipByMmsi, pairRanges, pairLinks, fcDashed, fleetCircles, legacyTargetsOrdered, legacyHits, legacyOverlayTargets, shipOverlayLayerData, pairRangesInteractive, pairLinksInteractive, fcLinesInteractive, fleetCirclesInteractive, overlays.pairRange, overlays.pairLines, overlays.fcLines, overlays.fleetCircles, settings.showDensity, settings.showShips, trackReplayDeckLayers, onDeckSelectOrHighlight, onSelectMmsi, onToggleHighlightMmsi, setDeckHoverPairs, clearMapFleetHoverState, setDeckHoverMmsi, clearDeckHoverMmsi, toFleetMmsiList, touchDeckHoverState, hasAuxiliarySelectModifier, ]); // Globe Deck overlay useEffect(() => { const map = mapRef.current; if (!map || projection !== 'globe' || projectionBusyRef.current) return; const deckTarget = globeDeckLayerRef.current; if (!deckTarget) return; if (!ENABLE_GLOBE_DECK_OVERLAYS) { try { deckTarget.setProps({ layers: [], getTooltip: undefined, onClick: undefined } as never); } catch { // ignore } return; } const globeLayers = buildGlobeDeckLayers({ pairRanges, pairLinks, fcDashed, fleetCircles, legacyTargetsOrdered, legacyHits, overlays, showShips: settings.showShips, selectedMmsi, isHighlightedFleet, isHighlightedPair, isHighlightedMmsi, touchDeckHoverState, setDeckHoverPairs, setDeckHoverMmsi, clearDeckHoverPairs, clearDeckHoverMmsi, clearMapFleetHoverState, setMapFleetHoverState, toFleetMmsiList, }); const normalizedLayers = sanitizeDeckLayerList(globeLayers); const globeDeckProps = { layers: normalizedLayers, getTooltip: undefined, onClick: undefined }; try { deckTarget.setProps(globeDeckProps as never); } catch (e) { console.error('Failed to apply globe deck props. Falling back to empty deck layer set.', e); try { deckTarget.setProps({ ...globeDeckProps, layers: [] as unknown[] } as never); } catch { // Ignore secondary failure. } } }, [ projection, pairRanges, pairLinks, fcDashed, fleetCircles, legacyTargetsOrdered, overlays.pairRange, overlays.pairLines, overlays.fcLines, overlays.fleetCircles, settings.showShips, selectedMmsi, isHighlightedFleet, isHighlightedPair, clearDeckHoverPairs, clearDeckHoverMmsi, clearMapFleetHoverState, setDeckHoverPairs, setDeckHoverMmsi, setMapFleetHoverState, toFleetMmsiList, touchDeckHoverState, legacyHits, ]); }