import { useEffect, useMemo, type MutableRefObject } from 'react'; import { HexagonLayer } from '@deck.gl/aggregation-layers'; import { IconLayer, LineLayer, ScatterplotLayer } from '@deck.gl/layers'; 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 { SHIP_ICON_MAPPING, FLAT_SHIP_ICON_SIZE, FLAT_SHIP_ICON_SIZE_SELECTED, FLAT_SHIP_ICON_SIZE_HIGHLIGHTED, FLAT_LEGACY_HALO_RADIUS, FLAT_LEGACY_HALO_RADIUS_SELECTED, FLAT_LEGACY_HALO_RADIUS_HIGHLIGHTED, EMPTY_MMSI_SET, DEPTH_DISABLED_PARAMS, GLOBE_OVERLAY_PARAMS, HALO_OUTLINE_COLOR, HALO_OUTLINE_COLOR_SELECTED, HALO_OUTLINE_COLOR_HIGHLIGHTED, PAIR_RANGE_NORMAL_DECK, PAIR_RANGE_WARN_DECK, PAIR_LINE_NORMAL_DECK, PAIR_LINE_WARN_DECK, FC_LINE_NORMAL_DECK, FC_LINE_SUSPICIOUS_DECK, FLEET_RANGE_LINE_DECK, FLEET_RANGE_FILL_DECK, PAIR_RANGE_NORMAL_DECK_HL, PAIR_RANGE_WARN_DECK_HL, PAIR_LINE_NORMAL_DECK_HL, PAIR_LINE_WARN_DECK_HL, FC_LINE_NORMAL_DECK_HL, FC_LINE_SUSPICIOUS_DECK_HL, FLEET_RANGE_LINE_DECK_HL, FLEET_RANGE_FILL_DECK_HL, } from '../constants'; import { toSafeNumber } from '../lib/setUtils'; import { getDisplayHeading, getShipColor } from '../lib/shipUtils'; import { getShipTooltipHtml, getPairLinkTooltipHtml, getFcLinkTooltipHtml, getRangeTooltipHtml, getFleetCircleTooltipHtml, } from '../lib/tooltips'; import { sanitizeDeckLayerList } from '../lib/mapCore'; export function useDeckLayers( mapRef: MutableRefObject, overlayRef: MutableRefObject, globeDeckLayerRef: MutableRefObject, projectionBusyRef: MutableRefObject, opts: { projection: MapProjectionId; settings: Map3DSettings; 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, 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: unknown[] = []; const overlayParams = DEPTH_DISABLED_PARAMS; const clearDeckHover = () => { touchDeckHoverState(false); }; const isTargetShip = (mmsi: number) => (legacyHits ? legacyHits.has(mmsi) : false); const shipOtherData: AisTarget[] = []; const shipTargetData: AisTarget[] = []; for (const t of shipLayerData) { if (isTargetShip(t.mmsi)) shipTargetData.push(t); else shipOtherData.push(t); } const shipOverlayOtherData: AisTarget[] = []; const shipOverlayTargetData: AisTarget[] = []; for (const t of shipOverlayLayerData) { if (isTargetShip(t.mmsi)) shipOverlayTargetData.push(t); else shipOverlayOtherData.push(t); } if (settings.showDensity) { layers.push( new HexagonLayer({ id: 'density', data: shipLayerData, pickable: true, extruded: true, radius: 2500, elevationScale: 35, coverage: 0.92, opacity: 0.35, getPosition: (d) => [d.lon, d.lat], }), ); } if (overlays.pairRange && pairRanges.length > 0) { layers.push( new ScatterplotLayer({ id: 'pair-range', data: pairRanges, pickable: true, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'meters', getRadius: (d) => d.radiusNm * 1852, radiusMinPixels: 10, lineWidthUnits: 'pixels', getLineWidth: () => 1, getLineColor: (d) => (d.warn ? PAIR_RANGE_WARN_DECK : PAIR_RANGE_NORMAL_DECK), getPosition: (d) => d.center, onHover: (info) => { if (!info.object) { clearDeckHover(); return; } touchDeckHoverState(true); const p = info.object as PairRangeCircle; setDeckHoverPairs([p.aMmsi, p.bMmsi]); setDeckHoverMmsi([p.aMmsi, p.bMmsi]); clearMapFleetHoverState(); }, onClick: (info) => { if (!info.object) { onSelectMmsi(null); return; } const obj = info.object as PairRangeCircle; const sourceEvent = (info as { srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null }).srcEvent; if (sourceEvent && hasAuxiliarySelectModifier(sourceEvent)) { onToggleHighlightMmsi?.(obj.aMmsi); onToggleHighlightMmsi?.(obj.bMmsi); return; } onDeckSelectOrHighlight({ mmsi: obj.aMmsi }); }, }), ); } if (overlays.pairLines && (pairLinks?.length ?? 0) > 0) { layers.push( new LineLayer({ id: 'pair-lines', data: pairLinks, pickable: true, parameters: overlayParams, getSourcePosition: (d) => d.from, getTargetPosition: (d) => d.to, getColor: (d) => (d.warn ? PAIR_LINE_WARN_DECK : PAIR_LINE_NORMAL_DECK), getWidth: (d) => (d.warn ? 2.2 : 1.4), widthUnits: 'pixels', onHover: (info) => { if (!info.object) { clearDeckHover(); return; } touchDeckHoverState(true); const obj = info.object as PairLink; setDeckHoverPairs([obj.aMmsi, obj.bMmsi]); setDeckHoverMmsi([obj.aMmsi, obj.bMmsi]); clearMapFleetHoverState(); }, onClick: (info) => { if (!info.object) return; const obj = info.object as PairLink; const sourceEvent = (info as { srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null }).srcEvent; if (sourceEvent && hasAuxiliarySelectModifier(sourceEvent)) { onToggleHighlightMmsi?.(obj.aMmsi); onToggleHighlightMmsi?.(obj.bMmsi); return; } onDeckSelectOrHighlight({ mmsi: obj.aMmsi }); }, }), ); } if (overlays.fcLines && fcDashed.length > 0) { layers.push( new LineLayer({ id: 'fc-lines', data: fcDashed, pickable: true, parameters: overlayParams, getSourcePosition: (d) => d.from, getTargetPosition: (d) => d.to, getColor: (d) => (d.suspicious ? FC_LINE_SUSPICIOUS_DECK : FC_LINE_NORMAL_DECK), getWidth: () => 1.3, widthUnits: 'pixels', onHover: (info) => { if (!info.object) { clearDeckHover(); return; } touchDeckHoverState(true); const obj = info.object as DashSeg; if (obj.fromMmsi == null || obj.toMmsi == null) { clearDeckHover(); return; } setDeckHoverPairs([obj.fromMmsi, obj.toMmsi]); setDeckHoverMmsi([obj.fromMmsi, obj.toMmsi]); clearMapFleetHoverState(); }, onClick: (info) => { if (!info.object) return; const obj = info.object as DashSeg; if (obj.fromMmsi == null || obj.toMmsi == null) return; const sourceEvent = (info as { srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null }).srcEvent; if (sourceEvent && hasAuxiliarySelectModifier(sourceEvent)) { onToggleHighlightMmsi?.(obj.fromMmsi); onToggleHighlightMmsi?.(obj.toMmsi); return; } onDeckSelectOrHighlight({ mmsi: obj.fromMmsi }); }, }), ); } if (overlays.fleetCircles && (fleetCircles?.length ?? 0) > 0) { layers.push( new ScatterplotLayer({ id: 'fleet-circles', data: fleetCircles, pickable: true, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'meters', getRadius: (d) => d.radiusNm * 1852, lineWidthUnits: 'pixels', getLineWidth: () => 1.1, getLineColor: () => FLEET_RANGE_LINE_DECK, getPosition: (d) => d.center, onHover: (info) => { if (!info.object) { clearDeckHover(); return; } touchDeckHoverState(true); const obj = info.object as FleetCircle; const list = toFleetMmsiList(obj.vesselMmsis); setMapFleetHoverState(obj.ownerKey || null, list); setDeckHoverMmsi(list); clearDeckHoverPairs(); }, onClick: (info) => { if (!info.object) return; const obj = info.object as FleetCircle; const list = toFleetMmsiList(obj.vesselMmsis); const sourceEvent = (info as { srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null }).srcEvent; if (sourceEvent && hasAuxiliarySelectModifier(sourceEvent)) { for (const mmsi of list) onToggleHighlightMmsi?.(mmsi); return; } const first = list[0]; if (first != null) onDeckSelectOrHighlight({ mmsi: first }); }, }), ); layers.push( new ScatterplotLayer({ id: 'fleet-circles-fill', data: fleetCircles, pickable: false, billboard: false, parameters: overlayParams, filled: true, stroked: false, radiusUnits: 'meters', getRadius: (d) => d.radiusNm * 1852, getFillColor: () => FLEET_RANGE_FILL_DECK, getPosition: (d) => d.center, }), ); } if (settings.showShips) { const shipOnHover = (info: PickingInfo) => { if (!info.object) { clearDeckHover(); return; } touchDeckHoverState(true); const obj = info.object as AisTarget; setDeckHoverMmsi([obj.mmsi]); clearDeckHoverPairs(); clearMapFleetHoverState(); }; const shipOnClick = (info: PickingInfo) => { if (!info.object) { onSelectMmsi(null); return; } onDeckSelectOrHighlight( { mmsi: (info.object as AisTarget).mmsi, srcEvent: (info as { srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null }).srcEvent, }, true, ); }; if (shipOtherData.length > 0) { layers.push( new IconLayer({ id: 'ships-other', data: shipOtherData, pickable: true, billboard: false, parameters: overlayParams, iconAtlas: '/assets/ship.svg', iconMapping: SHIP_ICON_MAPPING, getIcon: () => 'ship', getPosition: (d) => [d.lon, d.lat] as [number, number], getAngle: (d) => -getDisplayHeading({ cog: d.cog, heading: d.heading }), sizeUnits: 'pixels', getSize: () => FLAT_SHIP_ICON_SIZE, getColor: (d) => getShipColor(d, null, null, EMPTY_MMSI_SET), onHover: shipOnHover, onClick: shipOnClick, alphaCutoff: 0.05, }), ); } if (shipOverlayOtherData.length > 0) { layers.push( new IconLayer({ id: 'ships-overlay-other', data: shipOverlayOtherData, pickable: false, billboard: false, parameters: overlayParams, iconAtlas: '/assets/ship.svg', iconMapping: SHIP_ICON_MAPPING, getIcon: () => 'ship', getPosition: (d) => [d.lon, d.lat] as [number, number], getAngle: (d) => -getDisplayHeading({ cog: d.cog, heading: d.heading }), sizeUnits: 'pixels', getSize: (d) => { if (selectedMmsi != null && d.mmsi === selectedMmsi) return FLAT_SHIP_ICON_SIZE_SELECTED; if (shipHighlightSet.has(d.mmsi)) return FLAT_SHIP_ICON_SIZE_HIGHLIGHTED; return 0; }, getColor: (d) => getShipColor(d, selectedMmsi, null, shipHighlightSet), alphaCutoff: 0.05, }), ); } if (legacyTargetsOrdered.length > 0) { layers.push( new ScatterplotLayer({ id: 'legacy-halo', data: legacyTargetsOrdered, pickable: false, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'pixels', getRadius: () => FLAT_LEGACY_HALO_RADIUS, lineWidthUnits: 'pixels', getLineWidth: () => 2, getLineColor: () => HALO_OUTLINE_COLOR, getPosition: (d) => [d.lon, d.lat] as [number, number], }), ); } if (shipTargetData.length > 0) { layers.push( new IconLayer({ id: 'ships-target', data: shipTargetData, pickable: true, billboard: false, parameters: overlayParams, iconAtlas: '/assets/ship.svg', iconMapping: SHIP_ICON_MAPPING, getIcon: () => 'ship', getPosition: (d) => [d.lon, d.lat] as [number, number], getAngle: (d) => -getDisplayHeading({ cog: d.cog, heading: d.heading }), sizeUnits: 'pixels', getSize: () => FLAT_SHIP_ICON_SIZE, getColor: (d) => getShipColor(d, null, legacyHits?.get(d.mmsi)?.shipCode ?? null, EMPTY_MMSI_SET), onHover: shipOnHover, onClick: shipOnClick, alphaCutoff: 0.05, }), ); } } if (overlays.pairRange && pairRangesInteractive.length > 0) { layers.push(new ScatterplotLayer({ id: 'pair-range-overlay', data: pairRangesInteractive, pickable: false, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'meters', getRadius: (d) => d.radiusNm * 1852, radiusMinPixels: 10, lineWidthUnits: 'pixels', getLineWidth: () => 2.2, getLineColor: (d) => (d.warn ? PAIR_RANGE_WARN_DECK_HL : PAIR_RANGE_NORMAL_DECK_HL), getPosition: (d) => d.center })); } if (overlays.pairLines && pairLinksInteractive.length > 0) { layers.push(new LineLayer({ id: 'pair-lines-overlay', data: pairLinksInteractive, pickable: false, parameters: overlayParams, getSourcePosition: (d) => d.from, getTargetPosition: (d) => d.to, getColor: (d) => (d.warn ? PAIR_LINE_WARN_DECK_HL : PAIR_LINE_NORMAL_DECK_HL), getWidth: () => 2.6, widthUnits: 'pixels' })); } if (overlays.fcLines && fcLinesInteractive.length > 0) { layers.push(new LineLayer({ id: 'fc-lines-overlay', data: fcLinesInteractive, pickable: false, parameters: overlayParams, getSourcePosition: (d) => d.from, getTargetPosition: (d) => d.to, getColor: (d) => (d.suspicious ? FC_LINE_SUSPICIOUS_DECK_HL : FC_LINE_NORMAL_DECK_HL), getWidth: () => 1.9, widthUnits: 'pixels' })); } if (overlays.fleetCircles && fleetCirclesInteractive.length > 0) { layers.push(new ScatterplotLayer({ id: 'fleet-circles-overlay-fill', data: fleetCirclesInteractive, pickable: false, billboard: false, parameters: overlayParams, filled: true, stroked: false, radiusUnits: 'meters', getRadius: (d) => d.radiusNm * 1852, getFillColor: () => FLEET_RANGE_FILL_DECK_HL })); layers.push(new ScatterplotLayer({ id: 'fleet-circles-overlay', data: fleetCirclesInteractive, pickable: false, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'meters', getRadius: (d) => d.radiusNm * 1852, lineWidthUnits: 'pixels', getLineWidth: () => 1.8, getLineColor: () => FLEET_RANGE_LINE_DECK_HL, getPosition: (d) => d.center })); } if (settings.showShips && legacyOverlayTargets.length > 0) { layers.push(new ScatterplotLayer({ id: 'legacy-halo-overlay', data: legacyOverlayTargets, pickable: false, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'pixels', getRadius: (d) => { if (selectedMmsi && d.mmsi === selectedMmsi) return FLAT_LEGACY_HALO_RADIUS_SELECTED; return FLAT_LEGACY_HALO_RADIUS_HIGHLIGHTED; }, lineWidthUnits: 'pixels', getLineWidth: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? 2.5 : 2.2), getLineColor: (d) => { if (selectedMmsi && d.mmsi === selectedMmsi) return HALO_OUTLINE_COLOR_SELECTED; if (shipHighlightSet.has(d.mmsi)) return HALO_OUTLINE_COLOR_HIGHLIGHTED; return HALO_OUTLINE_COLOR; }, getPosition: (d) => [d.lon, d.lat] as [number, number] })); } if (settings.showShips && shipOverlayLayerData.filter((t) => legacyHits?.has(t.mmsi)).length > 0) { const shipOverlayTargetData2 = shipOverlayLayerData.filter((t) => legacyHits?.has(t.mmsi)); layers.push(new IconLayer({ id: 'ships-overlay-target', data: shipOverlayTargetData2, pickable: false, billboard: false, parameters: overlayParams, iconAtlas: '/assets/ship.svg', iconMapping: SHIP_ICON_MAPPING, getIcon: () => 'ship', getPosition: (d) => [d.lon, d.lat] as [number, number], getAngle: (d) => -getDisplayHeading({ cog: d.cog, heading: d.heading }), sizeUnits: 'pixels', getSize: (d) => { if (selectedMmsi != null && d.mmsi === selectedMmsi) return FLAT_SHIP_ICON_SIZE_SELECTED; if (shipHighlightSet.has(d.mmsi)) return FLAT_SHIP_ICON_SIZE_HIGHLIGHTED; return 0; }, getColor: (d) => { if (!shipHighlightSet.has(d.mmsi) && !(selectedMmsi != null && d.mmsi === selectedMmsi)) return [0, 0, 0, 0]; return getShipColor(d, selectedMmsi, legacyHits?.get(d.mmsi)?.shipCode ?? null, shipHighlightSet); } })); } const normalizedLayers = sanitizeDeckLayerList(layers); 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. Falling back to empty layer set.', e); try { deckTarget.setProps({ ...deckProps, layers: [] as unknown[] } as never); } catch { // Ignore secondary failure. } } }, [ 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, 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; const overlayParams = GLOBE_OVERLAY_PARAMS; const globeLayers: unknown[] = []; if (overlays.pairRange && pairRanges.length > 0) { globeLayers.push(new ScatterplotLayer({ id: 'pair-range-globe', data: pairRanges, pickable: true, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'meters', getRadius: (d) => d.radiusNm * 1852, radiusMinPixels: 10, lineWidthUnits: 'pixels', getLineWidth: (d) => (isHighlightedPair(d.aMmsi, d.bMmsi) ? 2.2 : 1), getLineColor: (d) => { const hl = isHighlightedPair(d.aMmsi, d.bMmsi); if (hl) return d.warn ? PAIR_RANGE_WARN_DECK_HL : PAIR_RANGE_NORMAL_DECK_HL; return d.warn ? PAIR_RANGE_WARN_DECK : PAIR_RANGE_NORMAL_DECK; }, getPosition: (d) => d.center, onHover: (info) => { if (!info.object) { clearDeckHoverPairs(); clearDeckHoverMmsi(); return; } touchDeckHoverState(true); const p = info.object as PairRangeCircle; setDeckHoverPairs([p.aMmsi, p.bMmsi]); setDeckHoverMmsi([p.aMmsi, p.bMmsi]); clearMapFleetHoverState(); } })); } if (overlays.pairLines && (pairLinks?.length ?? 0) > 0) { const links = pairLinks || []; globeLayers.push(new LineLayer({ id: 'pair-lines-globe', data: links, pickable: true, parameters: overlayParams, getSourcePosition: (d) => d.from, getTargetPosition: (d) => d.to, getColor: (d) => { const hl = isHighlightedPair(d.aMmsi, d.bMmsi); if (hl) return d.warn ? PAIR_LINE_WARN_DECK_HL : PAIR_LINE_NORMAL_DECK_HL; return d.warn ? PAIR_LINE_WARN_DECK : PAIR_LINE_NORMAL_DECK; }, getWidth: (d) => (isHighlightedPair(d.aMmsi, d.bMmsi) ? 2.6 : d.warn ? 2.2 : 1.4), widthUnits: 'pixels', onHover: (info) => { if (!info.object) { clearDeckHoverPairs(); clearDeckHoverMmsi(); return; } touchDeckHoverState(true); const obj = info.object as PairLink; setDeckHoverPairs([obj.aMmsi, obj.bMmsi]); setDeckHoverMmsi([obj.aMmsi, obj.bMmsi]); clearMapFleetHoverState(); } })); } if (overlays.fcLines && fcDashed.length > 0) { globeLayers.push(new LineLayer({ id: 'fc-lines-globe', data: fcDashed, pickable: true, parameters: overlayParams, getSourcePosition: (d) => d.from, getTargetPosition: (d) => d.to, getColor: (d) => { const ih = [d.fromMmsi, d.toMmsi].some((v) => isHighlightedMmsi(v ?? -1)); if (ih) return d.suspicious ? FC_LINE_SUSPICIOUS_DECK_HL : FC_LINE_NORMAL_DECK_HL; return d.suspicious ? FC_LINE_SUSPICIOUS_DECK : FC_LINE_NORMAL_DECK; }, getWidth: (d) => { const ih = [d.fromMmsi, d.toMmsi].some((v) => isHighlightedMmsi(v ?? -1)); return ih ? 1.9 : 1.3; }, widthUnits: 'pixels', onHover: (info) => { if (!info.object) { clearDeckHoverPairs(); clearDeckHoverMmsi(); return; } touchDeckHoverState(true); const obj = info.object as DashSeg; if (obj.fromMmsi == null || obj.toMmsi == null) { clearDeckHoverPairs(); clearDeckHoverMmsi(); return; } setDeckHoverPairs([obj.fromMmsi, obj.toMmsi]); setDeckHoverMmsi([obj.fromMmsi, obj.toMmsi]); clearMapFleetHoverState(); } })); } if (overlays.fleetCircles && (fleetCircles?.length ?? 0) > 0) { const circles = fleetCircles || []; globeLayers.push(new ScatterplotLayer({ id: 'fleet-circles-globe', data: circles, pickable: true, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'meters', getRadius: (d) => d.radiusNm * 1852, lineWidthUnits: 'pixels', getLineWidth: (d) => (isHighlightedFleet(d.ownerKey, d.vesselMmsis) ? 1.8 : 1.1), getLineColor: (d) => (isHighlightedFleet(d.ownerKey, d.vesselMmsis) ? FLEET_RANGE_LINE_DECK_HL : FLEET_RANGE_LINE_DECK), getPosition: (d) => d.center, onHover: (info) => { if (!info.object) { clearDeckHoverPairs(); clearDeckHoverMmsi(); clearMapFleetHoverState(); return; } touchDeckHoverState(true); const obj = info.object as FleetCircle; const list = toFleetMmsiList(obj.vesselMmsis); setMapFleetHoverState(obj.ownerKey || null, list); setDeckHoverMmsi(list); clearDeckHoverPairs(); } })); globeLayers.push(new ScatterplotLayer({ id: 'fleet-circles-fill-globe', data: circles, pickable: false, billboard: false, parameters: overlayParams, filled: true, stroked: false, radiusUnits: 'meters', getRadius: (d) => d.radiusNm * 1852, getFillColor: (d) => (isHighlightedFleet(d.ownerKey, d.vesselMmsis) ? FLEET_RANGE_FILL_DECK_HL : FLEET_RANGE_FILL_DECK), getPosition: (d) => d.center })); } if (settings.showShips && legacyTargetsOrdered.length > 0) { globeLayers.push(new ScatterplotLayer({ id: 'legacy-halo-globe', data: legacyTargetsOrdered, pickable: false, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'pixels', getRadius: (d) => { if (selectedMmsi && d.mmsi === selectedMmsi) return FLAT_LEGACY_HALO_RADIUS_SELECTED; return FLAT_LEGACY_HALO_RADIUS; }, lineWidthUnits: 'pixels', getLineWidth: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? 2.5 : 2), getLineColor: (d) => { if (selectedMmsi && d.mmsi === selectedMmsi) return HALO_OUTLINE_COLOR_SELECTED; return HALO_OUTLINE_COLOR; }, getPosition: (d) => [d.lon, d.lat] as [number, number] })); } 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, ]); }