- MapLibre 중첩 interpolate 표현식 에러 수정 - 6레이어 구조: hitarea, casing, line, glow, points, label - 호버 시 flat value 사용 (case 내 interpolate 제거) - Globe/Mercator 양쪽 프로젝션 레이어 순서 지원 - 진한 색상, 굵은 라인, 포인트 마커로 시인성 향상 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
529 lines
20 KiB
TypeScript
529 lines
20 KiB
TypeScript
import { MapboxOverlay } from '@deck.gl/mapbox';
|
|
import maplibregl from 'maplibre-gl';
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import type { AisTarget } from '../../entities/aisTarget/model/types';
|
|
import { MaplibreDeckCustomLayer } from './MaplibreDeckCustomLayer';
|
|
import type { BaseMapId, Map3DProps, MapProjectionId } from './types';
|
|
import type { DashSeg, PairRangeCircle } from './types';
|
|
import {
|
|
mergeNumberSets,
|
|
makeSetSignature,
|
|
isFiniteNumber,
|
|
toIntMmsi,
|
|
makeUniqueSorted,
|
|
equalNumberArrays,
|
|
} from './lib/setUtils';
|
|
import { dashifyLine } from './lib/dashifyLine';
|
|
import { useHoverState } from './hooks/useHoverState';
|
|
import { useMapInit } from './hooks/useMapInit';
|
|
import { useProjectionToggle } from './hooks/useProjectionToggle';
|
|
import { useBaseMapToggle } from './hooks/useBaseMapToggle';
|
|
import { useFlyTo } from './hooks/useFlyTo';
|
|
import { useZonesLayer } from './hooks/useZonesLayer';
|
|
import { usePredictionVectors } from './hooks/usePredictionVectors';
|
|
import { useGlobeShips } from './hooks/useGlobeShips';
|
|
import { useGlobeOverlays } from './hooks/useGlobeOverlays';
|
|
import { useGlobeInteraction } from './hooks/useGlobeInteraction';
|
|
import { useDeckLayers } from './hooks/useDeckLayers';
|
|
import { useSubcablesLayer } from './hooks/useSubcablesLayer';
|
|
|
|
export type { Map3DSettings, BaseMapId, MapProjectionId } from './types';
|
|
|
|
type Props = Map3DProps;
|
|
|
|
|
|
export function Map3D({
|
|
targets,
|
|
zones,
|
|
selectedMmsi,
|
|
hoveredMmsiSet = [],
|
|
hoveredFleetMmsiSet = [],
|
|
hoveredPairMmsiSet = [],
|
|
hoveredFleetOwnerKey = null,
|
|
highlightedMmsiSet = [],
|
|
settings,
|
|
baseMap,
|
|
projection,
|
|
overlays,
|
|
onSelectMmsi,
|
|
onToggleHighlightMmsi,
|
|
onViewBboxChange,
|
|
legacyHits,
|
|
pairLinks,
|
|
fcLinks,
|
|
fleetCircles,
|
|
onProjectionLoadingChange,
|
|
fleetFocus,
|
|
onHoverFleet,
|
|
onClearFleetHover,
|
|
onHoverMmsi,
|
|
onClearMmsiHover,
|
|
onHoverPair,
|
|
onClearPairHover,
|
|
subcableGeo = null,
|
|
hoveredCableId = null,
|
|
onHoverCable,
|
|
onClickCable,
|
|
}: Props) {
|
|
void onHoverFleet;
|
|
void onClearFleetHover;
|
|
void onHoverMmsi;
|
|
void onClearMmsiHover;
|
|
void onHoverPair;
|
|
void onClearPairHover;
|
|
|
|
// ── Shared refs ──────────────────────────────────────────────────────
|
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
const mapRef = useRef<maplibregl.Map | null>(null);
|
|
const overlayRef = useRef<MapboxOverlay | null>(null);
|
|
const overlayInteractionRef = useRef<MapboxOverlay | null>(null);
|
|
const globeDeckLayerRef = useRef<MaplibreDeckCustomLayer | null>(null);
|
|
const baseMapRef = useRef<BaseMapId>(baseMap);
|
|
const projectionRef = useRef<MapProjectionId>(projection);
|
|
const projectionBusyRef = useRef(false);
|
|
const deckHoverRafRef = useRef<number | null>(null);
|
|
const deckHoverHasHitRef = useRef(false);
|
|
|
|
useEffect(() => { baseMapRef.current = baseMap; }, [baseMap]);
|
|
useEffect(() => { projectionRef.current = projection; }, [projection]);
|
|
|
|
// ── Hover state ──────────────────────────────────────────────────────
|
|
const {
|
|
setHoveredDeckMmsiSet,
|
|
setHoveredDeckPairMmsiSet,
|
|
setHoveredDeckFleetOwnerKey,
|
|
setHoveredDeckFleetMmsiSet,
|
|
hoveredZoneId, setHoveredZoneId,
|
|
hoveredMmsiSetRef, hoveredFleetMmsiSetRef, hoveredPairMmsiSetRef,
|
|
externalHighlightedSetRef,
|
|
hoveredDeckMmsiSetRef, hoveredDeckPairMmsiSetRef, hoveredDeckFleetMmsiSetRef,
|
|
hoveredFleetOwnerKeys,
|
|
} = useHoverState({
|
|
hoveredMmsiSet, hoveredFleetMmsiSet, hoveredPairMmsiSet,
|
|
hoveredFleetOwnerKey, highlightedMmsiSet,
|
|
});
|
|
|
|
const fleetFocusId = fleetFocus?.id;
|
|
const fleetFocusLon = fleetFocus?.center?.[0];
|
|
const fleetFocusLat = fleetFocus?.center?.[1];
|
|
const fleetFocusZoom = fleetFocus?.zoom;
|
|
|
|
// ── Highlight memos ──────────────────────────────────────────────────
|
|
const effectiveHoveredPairMmsiSet = useMemo(
|
|
() => mergeNumberSets(hoveredPairMmsiSetRef, hoveredDeckPairMmsiSetRef),
|
|
[hoveredPairMmsiSetRef, hoveredDeckPairMmsiSetRef],
|
|
);
|
|
const effectiveHoveredFleetMmsiSet = useMemo(
|
|
() => mergeNumberSets(hoveredFleetMmsiSetRef, hoveredDeckFleetMmsiSetRef),
|
|
[hoveredFleetMmsiSetRef, hoveredDeckFleetMmsiSetRef],
|
|
);
|
|
const [mapSyncEpoch, setMapSyncEpoch] = useState(0);
|
|
const highlightedMmsiSetCombined = useMemo(
|
|
() =>
|
|
mergeNumberSets(
|
|
hoveredMmsiSetRef,
|
|
hoveredDeckMmsiSetRef,
|
|
externalHighlightedSetRef,
|
|
effectiveHoveredFleetMmsiSet,
|
|
effectiveHoveredPairMmsiSet,
|
|
),
|
|
[
|
|
hoveredMmsiSetRef,
|
|
hoveredDeckMmsiSetRef,
|
|
externalHighlightedSetRef,
|
|
effectiveHoveredFleetMmsiSet,
|
|
effectiveHoveredPairMmsiSet,
|
|
],
|
|
);
|
|
const highlightedMmsiSetForShips = useMemo(
|
|
() => (projection === 'globe' ? mergeNumberSets(hoveredMmsiSetRef, externalHighlightedSetRef) : highlightedMmsiSetCombined),
|
|
[projection, hoveredMmsiSetRef, externalHighlightedSetRef, highlightedMmsiSetCombined],
|
|
);
|
|
const hoveredShipSignature = useMemo(
|
|
() =>
|
|
`${makeSetSignature(hoveredMmsiSetRef)}|${makeSetSignature(externalHighlightedSetRef)}|${makeSetSignature(
|
|
hoveredDeckMmsiSetRef,
|
|
)}|${makeSetSignature(effectiveHoveredFleetMmsiSet)}|${makeSetSignature(effectiveHoveredPairMmsiSet)}`,
|
|
[
|
|
hoveredMmsiSetRef,
|
|
externalHighlightedSetRef,
|
|
hoveredDeckMmsiSetRef,
|
|
effectiveHoveredFleetMmsiSet,
|
|
effectiveHoveredPairMmsiSet,
|
|
],
|
|
);
|
|
void hoveredShipSignature;
|
|
const hoveredPairMmsiList = useMemo(() => makeUniqueSorted(Array.from(effectiveHoveredPairMmsiSet)), [effectiveHoveredPairMmsiSet]);
|
|
const hoveredFleetOwnerKeyList = useMemo(() => Array.from(hoveredFleetOwnerKeys).sort(), [hoveredFleetOwnerKeys]);
|
|
const hoveredFleetMmsiList = useMemo(() => makeUniqueSorted(Array.from(effectiveHoveredFleetMmsiSet)), [effectiveHoveredFleetMmsiSet]);
|
|
|
|
const isHighlightedMmsi = useCallback(
|
|
(mmsi: number) => highlightedMmsiSetCombined.has(mmsi),
|
|
[highlightedMmsiSetCombined],
|
|
);
|
|
const baseHighlightedMmsiSet = useMemo(() => {
|
|
const out = new Set<number>();
|
|
if (selectedMmsi != null) out.add(selectedMmsi);
|
|
externalHighlightedSetRef.forEach((value) => { out.add(value); });
|
|
return out;
|
|
}, [selectedMmsi, externalHighlightedSetRef]);
|
|
const isBaseHighlightedMmsi = useCallback(
|
|
(mmsi: number) => baseHighlightedMmsiSet.has(mmsi),
|
|
[baseHighlightedMmsiSet],
|
|
);
|
|
const isHighlightedPair = useCallback(
|
|
(aMmsi: number, bMmsi: number) =>
|
|
effectiveHoveredPairMmsiSet.size === 2 &&
|
|
effectiveHoveredPairMmsiSet.has(aMmsi) &&
|
|
effectiveHoveredPairMmsiSet.has(bMmsi),
|
|
[effectiveHoveredPairMmsiSet],
|
|
);
|
|
const isHighlightedFleet = useCallback(
|
|
(ownerKey: string, vesselMmsis: number[]) => {
|
|
if (hoveredFleetOwnerKeys.has(ownerKey)) return true;
|
|
return vesselMmsis.some((x) => isHighlightedMmsi(x));
|
|
},
|
|
[hoveredFleetOwnerKeys, isHighlightedMmsi],
|
|
);
|
|
|
|
// ── Ship data memos ──────────────────────────────────────────────────
|
|
const shipData = useMemo(() => {
|
|
return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon) && isFiniteNumber(t.mmsi));
|
|
}, [targets]);
|
|
|
|
const shipByMmsi = useMemo(() => {
|
|
const byMmsi = new Map<number, AisTarget>();
|
|
for (const t of shipData) byMmsi.set(t.mmsi, t);
|
|
return byMmsi;
|
|
}, [shipData]);
|
|
|
|
const shipLayerData = useMemo(() => {
|
|
if (shipData.length === 0) return shipData;
|
|
return [...shipData];
|
|
}, [shipData]);
|
|
|
|
const shipHighlightSet = useMemo(() => {
|
|
const out = new Set(highlightedMmsiSetForShips);
|
|
if (selectedMmsi) out.add(selectedMmsi);
|
|
return out;
|
|
}, [highlightedMmsiSetForShips, selectedMmsi]);
|
|
|
|
const shipHoverOverlaySet = useMemo(
|
|
() =>
|
|
projection === 'globe'
|
|
? mergeNumberSets(highlightedMmsiSetCombined, shipHighlightSet)
|
|
: shipHighlightSet,
|
|
[projection, highlightedMmsiSetCombined, shipHighlightSet],
|
|
);
|
|
|
|
const shipOverlayLayerData = useMemo(() => {
|
|
if (shipLayerData.length === 0) return [];
|
|
if (shipHighlightSet.size === 0) return [];
|
|
return shipLayerData.filter((target) => shipHighlightSet.has(target.mmsi));
|
|
}, [shipHighlightSet, shipLayerData]);
|
|
|
|
// ── Deck hover management ────────────────────────────────────────────
|
|
const hasAuxiliarySelectModifier = useCallback(
|
|
(ev?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null): boolean => {
|
|
if (!ev) return false;
|
|
return !!(ev.shiftKey || ev.ctrlKey || ev.metaKey);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const toFleetMmsiList = useCallback((value: unknown) => {
|
|
if (!Array.isArray(value)) return [];
|
|
const out: number[] = [];
|
|
for (const item of value) {
|
|
const v = toIntMmsi(item);
|
|
if (v != null) out.push(v);
|
|
}
|
|
return out;
|
|
}, []);
|
|
|
|
const onDeckSelectOrHighlight = useCallback(
|
|
(info: unknown, allowMultiSelect = false) => {
|
|
const obj = info as {
|
|
mmsi?: unknown;
|
|
srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null;
|
|
};
|
|
const mmsi = toIntMmsi(obj.mmsi);
|
|
if (mmsi == null) return;
|
|
const evt = obj.srcEvent ?? null;
|
|
const isAux = hasAuxiliarySelectModifier(evt);
|
|
if (onToggleHighlightMmsi && isAux) {
|
|
onToggleHighlightMmsi(mmsi);
|
|
return;
|
|
}
|
|
if (!allowMultiSelect && selectedMmsi === mmsi) {
|
|
onSelectMmsi(null);
|
|
return;
|
|
}
|
|
onSelectMmsi(mmsi);
|
|
},
|
|
[hasAuxiliarySelectModifier, onSelectMmsi, onToggleHighlightMmsi, selectedMmsi],
|
|
);
|
|
|
|
// eslint-disable-next-line react-hooks/preserve-manual-memoization
|
|
const setHoveredDeckFleetMmsis = useCallback((next: number[]) => {
|
|
const normalized = makeUniqueSorted(next);
|
|
setHoveredDeckFleetMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
|
}, []);
|
|
|
|
// eslint-disable-next-line react-hooks/preserve-manual-memoization
|
|
const setHoveredDeckFleetOwner = useCallback((ownerKey: string | null) => {
|
|
setHoveredDeckFleetOwnerKey((prev) => (prev === ownerKey ? prev : ownerKey));
|
|
}, []);
|
|
|
|
const mapDeckMmsiHoverRef = useRef<number[]>([]);
|
|
const mapDeckPairHoverRef = useRef<number[]>([]);
|
|
const mapFleetHoverStateRef = useRef<{ ownerKey: string | null; vesselMmsis: number[] }>({
|
|
ownerKey: null,
|
|
vesselMmsis: [],
|
|
});
|
|
|
|
const clearMapFleetHoverState = useCallback(() => {
|
|
mapFleetHoverStateRef.current = { ownerKey: null, vesselMmsis: [] };
|
|
setHoveredDeckFleetOwner(null);
|
|
setHoveredDeckFleetMmsis([]);
|
|
}, [setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis]);
|
|
|
|
const clearDeckHoverPairs = useCallback(() => {
|
|
mapDeckPairHoverRef.current = [];
|
|
setHoveredDeckPairMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
|
|
}, [setHoveredDeckPairMmsiSet]);
|
|
|
|
const clearDeckHoverMmsi = useCallback(() => {
|
|
mapDeckMmsiHoverRef.current = [];
|
|
setHoveredDeckMmsiSet((prevState) => (prevState.length === 0 ? prevState : []));
|
|
}, [setHoveredDeckMmsiSet]);
|
|
|
|
const scheduleDeckHoverResolve = useCallback(() => {
|
|
if (deckHoverRafRef.current != null) return;
|
|
deckHoverRafRef.current = window.requestAnimationFrame(() => {
|
|
deckHoverRafRef.current = null;
|
|
if (!deckHoverHasHitRef.current) {
|
|
clearDeckHoverMmsi();
|
|
clearDeckHoverPairs();
|
|
clearMapFleetHoverState();
|
|
}
|
|
deckHoverHasHitRef.current = false;
|
|
});
|
|
}, [clearDeckHoverMmsi, clearDeckHoverPairs, clearMapFleetHoverState]);
|
|
|
|
const touchDeckHoverState = useCallback(
|
|
(isHover: boolean) => {
|
|
if (isHover) deckHoverHasHitRef.current = true;
|
|
scheduleDeckHoverResolve();
|
|
},
|
|
[scheduleDeckHoverResolve],
|
|
);
|
|
|
|
const setDeckHoverMmsi = useCallback(
|
|
(next: number[]) => {
|
|
const normalized = makeUniqueSorted(next);
|
|
touchDeckHoverState(normalized.length > 0);
|
|
setHoveredDeckMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
|
mapDeckMmsiHoverRef.current = normalized;
|
|
},
|
|
[setHoveredDeckMmsiSet, touchDeckHoverState],
|
|
);
|
|
|
|
const setDeckHoverPairs = useCallback(
|
|
(next: number[]) => {
|
|
const normalized = makeUniqueSorted(next);
|
|
touchDeckHoverState(normalized.length > 0);
|
|
setHoveredDeckPairMmsiSet((prev) => (equalNumberArrays(prev, normalized) ? prev : normalized));
|
|
mapDeckPairHoverRef.current = normalized;
|
|
},
|
|
[setHoveredDeckPairMmsiSet, touchDeckHoverState],
|
|
);
|
|
|
|
const setMapFleetHoverState = useCallback(
|
|
(ownerKey: string | null, vesselMmsis: number[]) => {
|
|
const normalized = makeUniqueSorted(vesselMmsis);
|
|
const prev = mapFleetHoverStateRef.current;
|
|
if (prev.ownerKey === ownerKey && equalNumberArrays(prev.vesselMmsis, normalized)) {
|
|
return;
|
|
}
|
|
touchDeckHoverState(!!ownerKey || normalized.length > 0);
|
|
setHoveredDeckFleetOwner(ownerKey);
|
|
setHoveredDeckFleetMmsis(normalized);
|
|
mapFleetHoverStateRef.current = { ownerKey, vesselMmsis: normalized };
|
|
},
|
|
[setHoveredDeckFleetOwner, setHoveredDeckFleetMmsis, touchDeckHoverState],
|
|
);
|
|
|
|
// hover RAF cleanup
|
|
useEffect(() => {
|
|
return () => {
|
|
if (deckHoverRafRef.current != null) {
|
|
window.cancelAnimationFrame(deckHoverRafRef.current);
|
|
deckHoverRafRef.current = null;
|
|
}
|
|
deckHoverHasHitRef.current = false;
|
|
};
|
|
}, []);
|
|
|
|
// sync external fleet hover state
|
|
useEffect(() => {
|
|
mapFleetHoverStateRef.current = {
|
|
ownerKey: hoveredFleetOwnerKey,
|
|
vesselMmsis: hoveredFleetMmsiSet,
|
|
};
|
|
}, [hoveredFleetOwnerKey, hoveredFleetMmsiSet]);
|
|
|
|
// ── Overlay data memos ───────────────────────────────────────────────
|
|
const fcDashed = useMemo(() => {
|
|
const segs: DashSeg[] = [];
|
|
for (const l of fcLinks || []) {
|
|
segs.push(...dashifyLine(l.from, l.to, l.suspicious, l.distanceNm, l.fcMmsi, l.otherMmsi));
|
|
}
|
|
return segs;
|
|
}, [fcLinks]);
|
|
|
|
const pairRanges = useMemo(() => {
|
|
const out: PairRangeCircle[] = [];
|
|
for (const p of pairLinks || []) {
|
|
const center: [number, number] = [(p.from[0] + p.to[0]) / 2, (p.from[1] + p.to[1]) / 2];
|
|
out.push({
|
|
center,
|
|
radiusNm: Math.max(0.05, p.distanceNm / 2),
|
|
warn: p.warn,
|
|
aMmsi: p.aMmsi,
|
|
bMmsi: p.bMmsi,
|
|
distanceNm: p.distanceNm,
|
|
});
|
|
}
|
|
return out;
|
|
}, [pairLinks]);
|
|
|
|
const pairLinksInteractive = useMemo(() => {
|
|
if (!overlays.pairLines || (pairLinks?.length ?? 0) === 0) return [];
|
|
if (hoveredPairMmsiSetRef.size < 2) return [];
|
|
const links = pairLinks || [];
|
|
return links.filter((link) =>
|
|
hoveredPairMmsiSetRef.has(link.aMmsi) && hoveredPairMmsiSetRef.has(link.bMmsi),
|
|
);
|
|
}, [pairLinks, hoveredPairMmsiSetRef, overlays.pairLines]);
|
|
|
|
const pairRangesInteractive = useMemo(() => {
|
|
if (!overlays.pairRange || pairRanges.length === 0) return [];
|
|
if (hoveredPairMmsiSetRef.size < 2) return [];
|
|
return pairRanges.filter((range) =>
|
|
hoveredPairMmsiSetRef.has(range.aMmsi) && hoveredPairMmsiSetRef.has(range.bMmsi),
|
|
);
|
|
}, [pairRanges, hoveredPairMmsiSetRef, overlays.pairRange]);
|
|
|
|
const fcLinesInteractive = useMemo(() => {
|
|
if (!overlays.fcLines || fcDashed.length === 0) return [];
|
|
if (highlightedMmsiSetCombined.size === 0) return [];
|
|
return fcDashed.filter(
|
|
(line) =>
|
|
[line.fromMmsi, line.toMmsi].some((mmsi) => (mmsi == null ? false : highlightedMmsiSetCombined.has(mmsi))),
|
|
);
|
|
}, [fcDashed, overlays.fcLines, highlightedMmsiSetCombined]);
|
|
|
|
const fleetCirclesInteractive = useMemo(() => {
|
|
if (!overlays.fleetCircles || (fleetCircles?.length ?? 0) === 0) return [];
|
|
if (hoveredFleetOwnerKeys.size === 0 && highlightedMmsiSetCombined.size === 0) return [];
|
|
const circles = fleetCircles || [];
|
|
return circles.filter((item) => isHighlightedFleet(item.ownerKey, item.vesselMmsis));
|
|
}, [fleetCircles, hoveredFleetOwnerKeys, isHighlightedFleet, overlays.fleetCircles, highlightedMmsiSetCombined]);
|
|
|
|
// ── Hook orchestration ───────────────────────────────────────────────
|
|
const { ensureMercatorOverlay, clearGlobeNativeLayers, pulseMapSync } = useMapInit(
|
|
containerRef, mapRef, overlayRef, overlayInteractionRef, globeDeckLayerRef,
|
|
baseMapRef, projectionRef,
|
|
{ baseMap, projection, showSeamark: settings.showSeamark, onViewBboxChange, setMapSyncEpoch },
|
|
);
|
|
|
|
const reorderGlobeFeatureLayers = useProjectionToggle(
|
|
mapRef, overlayRef, overlayInteractionRef, globeDeckLayerRef, projectionBusyRef,
|
|
{ projection, clearGlobeNativeLayers, ensureMercatorOverlay, onProjectionLoadingChange, pulseMapSync, setMapSyncEpoch },
|
|
);
|
|
|
|
useBaseMapToggle(
|
|
mapRef,
|
|
{ baseMap, projection, showSeamark: settings.showSeamark, mapSyncEpoch, pulseMapSync },
|
|
);
|
|
|
|
useZonesLayer(
|
|
mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
|
|
{ zones, overlays, projection, baseMap, hoveredZoneId, mapSyncEpoch },
|
|
);
|
|
|
|
usePredictionVectors(
|
|
mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
|
|
{
|
|
overlays, settings, shipData, legacyHits, selectedMmsi,
|
|
externalHighlightedSetRef, projection, baseMap, mapSyncEpoch,
|
|
},
|
|
);
|
|
|
|
useGlobeShips(
|
|
mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
|
|
{
|
|
projection, settings, shipData, shipHighlightSet, shipHoverOverlaySet,
|
|
shipOverlayLayerData, shipLayerData, shipByMmsi, mapSyncEpoch,
|
|
onSelectMmsi, onToggleHighlightMmsi, targets, overlays,
|
|
legacyHits, selectedMmsi, isBaseHighlightedMmsi, hasAuxiliarySelectModifier,
|
|
},
|
|
);
|
|
|
|
useGlobeOverlays(
|
|
mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
|
|
{
|
|
overlays, pairLinks, fcLinks, fleetCircles, projection,
|
|
mapSyncEpoch, hoveredFleetMmsiList, hoveredFleetOwnerKeyList, hoveredPairMmsiList,
|
|
},
|
|
);
|
|
|
|
useGlobeInteraction(
|
|
mapRef, projectionBusyRef,
|
|
{
|
|
projection, settings, overlays, targets, shipData, shipByMmsi, selectedMmsi,
|
|
hoveredZoneId, legacyHits,
|
|
clearDeckHoverPairs, clearDeckHoverMmsi, clearMapFleetHoverState,
|
|
setDeckHoverPairs, setDeckHoverMmsi, setMapFleetHoverState, setHoveredZoneId,
|
|
},
|
|
);
|
|
|
|
useDeckLayers(
|
|
mapRef, overlayRef, globeDeckLayerRef, projectionBusyRef,
|
|
{
|
|
projection, settings, shipLayerData, shipOverlayLayerData, shipData,
|
|
legacyHits, pairLinks, fcLinks, 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,
|
|
},
|
|
);
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const noopCable = useCallback((_: string | null) => {}, []);
|
|
|
|
useSubcablesLayer(
|
|
mapRef, projectionBusyRef, reorderGlobeFeatureLayers,
|
|
{
|
|
subcableGeo: subcableGeo ?? null,
|
|
overlays, projection, mapSyncEpoch,
|
|
hoveredCableId: hoveredCableId ?? null,
|
|
onHoverCable: onHoverCable ?? noopCable,
|
|
onClickCable: onClickCable ?? noopCable,
|
|
},
|
|
);
|
|
|
|
useFlyTo(
|
|
mapRef, projectionRef,
|
|
{ selectedMmsi, shipData, fleetFocusId, fleetFocusLon, fleetFocusLat, fleetFocusZoom },
|
|
);
|
|
|
|
return <div ref={containerRef} style={{ width: '100%', height: '100%' }} />;
|
|
}
|