refactor(map3d): useDeckLayers 레이어 생성 팩토리 추출
- buildMercatorDeckLayers: Mercator 모드 Deck.gl 레이어 팩토리 - buildGlobeDeckLayers: Globe 모드 Deck.gl 레이어 팩토리 - useDeckLayers: 오케스트레이션 + 툴팁/클릭 + setProps Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
e2dc927ad2
커밋
b1551f800b
@ -1,6 +1,4 @@
|
||||
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';
|
||||
@ -9,39 +7,7 @@ import type { FcLink, FleetCircle, PairLink } from '../../../features/legacyDash
|
||||
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,
|
||||
@ -50,7 +16,7 @@ import {
|
||||
getFleetCircleTooltipHtml,
|
||||
} from '../lib/tooltips';
|
||||
import { sanitizeDeckLayerList } from '../lib/mapCore';
|
||||
import { getCachedShipIcon } from '../lib/shipIconCache';
|
||||
import { buildMercatorDeckLayers, buildGlobeDeckLayers } from '../lib/deckLayerFactories';
|
||||
|
||||
// NOTE:
|
||||
// Globe mode now relies on MapLibre native overlays (useGlobeOverlays/useGlobeShips).
|
||||
@ -151,343 +117,37 @@ export function useDeckLayers(
|
||||
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<AisTarget>({
|
||||
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<PairRangeCircle>({
|
||||
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<PairLink>({
|
||||
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<DashSeg>({
|
||||
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<FleetCircle>({
|
||||
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<FleetCircle>({
|
||||
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<AisTarget>({
|
||||
id: 'ships-other',
|
||||
data: shipOtherData,
|
||||
pickable: true,
|
||||
billboard: false,
|
||||
parameters: overlayParams,
|
||||
iconAtlas: getCachedShipIcon(),
|
||||
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<AisTarget>({
|
||||
id: 'ships-overlay-other',
|
||||
data: shipOverlayOtherData,
|
||||
pickable: false,
|
||||
billboard: false,
|
||||
parameters: overlayParams,
|
||||
iconAtlas: getCachedShipIcon(),
|
||||
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<AisTarget>({
|
||||
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<AisTarget>({
|
||||
id: 'ships-target',
|
||||
data: shipTargetData,
|
||||
pickable: true,
|
||||
billboard: false,
|
||||
parameters: overlayParams,
|
||||
iconAtlas: getCachedShipIcon(),
|
||||
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 (pairRangesInteractive.length > 0) {
|
||||
layers.push(new ScatterplotLayer<PairRangeCircle>({ 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 (pairLinksInteractive.length > 0) {
|
||||
layers.push(new LineLayer<PairLink>({ 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 (fcLinesInteractive.length > 0) {
|
||||
layers.push(new LineLayer<DashSeg>({ 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 (fleetCirclesInteractive.length > 0) {
|
||||
layers.push(new ScatterplotLayer<FleetCircle>({ 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<FleetCircle>({ 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<AisTarget>({ 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<AisTarget>({ id: 'ships-overlay-target', data: shipOverlayTargetData2, pickable: false, billboard: false, parameters: overlayParams, iconAtlas: getCachedShipIcon(), 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 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);
|
||||
@ -610,32 +270,28 @@ export function useDeckLayers(
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const overlayParams = GLOBE_OVERLAY_PARAMS;
|
||||
const globeLayers: unknown[] = [];
|
||||
|
||||
if (overlays.pairRange && pairRanges.length > 0) {
|
||||
globeLayers.push(new ScatterplotLayer<PairRangeCircle>({ 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<PairLink>({ 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<DashSeg>({ 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<FleetCircle>({ 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<FleetCircle>({ 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<AisTarget>({ 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 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 };
|
||||
|
||||
485
apps/web/src/widgets/map3d/lib/deckLayerFactories.ts
Normal file
485
apps/web/src/widgets/map3d/lib/deckLayerFactories.ts
Normal file
@ -0,0 +1,485 @@
|
||||
import { HexagonLayer } from '@deck.gl/aggregation-layers';
|
||||
import { IconLayer, LineLayer, ScatterplotLayer } from '@deck.gl/layers';
|
||||
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 { FleetCircle, PairLink } from '../../../features/legacyDashboard/model/types';
|
||||
import type { MapToggleState } from '../../../features/mapToggles/MapToggles';
|
||||
import type { DashSeg, PairRangeCircle } from '../types';
|
||||
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 { getDisplayHeading, getShipColor } from './shipUtils';
|
||||
import { getCachedShipIcon } from './shipIconCache';
|
||||
|
||||
/* ── 공통 콜백 인터페이스 ─────────────────────────────── */
|
||||
|
||||
interface DeckHoverCallbacks {
|
||||
touchDeckHoverState: (isHover: boolean) => void;
|
||||
setDeckHoverPairs: (next: number[]) => void;
|
||||
setDeckHoverMmsi: (next: number[]) => void;
|
||||
clearDeckHoverPairs: () => void;
|
||||
clearMapFleetHoverState: () => void;
|
||||
setMapFleetHoverState: (ownerKey: string | null, vesselMmsis: number[]) => void;
|
||||
toFleetMmsiList: (value: unknown) => number[];
|
||||
}
|
||||
|
||||
interface DeckSelectCallbacks {
|
||||
hasAuxiliarySelectModifier: (ev?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null) => boolean;
|
||||
onSelectMmsi: (mmsi: number | null) => void;
|
||||
onToggleHighlightMmsi?: (mmsi: number) => void;
|
||||
onDeckSelectOrHighlight: (info: unknown, allowMultiSelect?: boolean) => void;
|
||||
}
|
||||
|
||||
/* ── Mercator Deck 레이어 ─────────────────────────────── */
|
||||
|
||||
export interface MercatorDeckLayerContext extends DeckHoverCallbacks, DeckSelectCallbacks {
|
||||
shipLayerData: AisTarget[];
|
||||
shipOverlayLayerData: AisTarget[];
|
||||
legacyTargetsOrdered: AisTarget[];
|
||||
legacyOverlayTargets: AisTarget[];
|
||||
legacyHits: Map<number, LegacyVesselInfo> | null | undefined;
|
||||
pairLinks: PairLink[] | undefined;
|
||||
fcDashed: DashSeg[];
|
||||
fleetCircles: FleetCircle[] | undefined;
|
||||
pairRanges: PairRangeCircle[];
|
||||
pairLinksInteractive: PairLink[];
|
||||
pairRangesInteractive: PairRangeCircle[];
|
||||
fcLinesInteractive: DashSeg[];
|
||||
fleetCirclesInteractive: FleetCircle[];
|
||||
overlays: MapToggleState;
|
||||
showDensity: boolean;
|
||||
showShips: boolean;
|
||||
selectedMmsi: number | null;
|
||||
shipHighlightSet: Set<number>;
|
||||
}
|
||||
|
||||
export function buildMercatorDeckLayers(ctx: MercatorDeckLayerContext): unknown[] {
|
||||
const layers: unknown[] = [];
|
||||
const overlayParams = DEPTH_DISABLED_PARAMS;
|
||||
const clearDeckHover = () => { ctx.touchDeckHoverState(false); };
|
||||
const isTargetShip = (mmsi: number) => (ctx.legacyHits ? ctx.legacyHits.has(mmsi) : false);
|
||||
|
||||
const shipOtherData: AisTarget[] = [];
|
||||
const shipTargetData: AisTarget[] = [];
|
||||
for (const t of ctx.shipLayerData) {
|
||||
if (isTargetShip(t.mmsi)) shipTargetData.push(t);
|
||||
else shipOtherData.push(t);
|
||||
}
|
||||
const shipOverlayOtherData: AisTarget[] = [];
|
||||
for (const t of ctx.shipOverlayLayerData) {
|
||||
if (!isTargetShip(t.mmsi)) shipOverlayOtherData.push(t);
|
||||
}
|
||||
|
||||
/* ─ density ─ */
|
||||
if (ctx.showDensity) {
|
||||
layers.push(
|
||||
new HexagonLayer<AisTarget>({
|
||||
id: 'density',
|
||||
data: ctx.shipLayerData,
|
||||
pickable: true,
|
||||
extruded: true,
|
||||
radius: 2500,
|
||||
elevationScale: 35,
|
||||
coverage: 0.92,
|
||||
opacity: 0.35,
|
||||
getPosition: (d) => [d.lon, d.lat],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/* ─ pair range ─ */
|
||||
if (ctx.overlays.pairRange && ctx.pairRanges.length > 0) {
|
||||
layers.push(
|
||||
new ScatterplotLayer<PairRangeCircle>({
|
||||
id: 'pair-range',
|
||||
data: ctx.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; }
|
||||
ctx.touchDeckHoverState(true);
|
||||
const p = info.object as PairRangeCircle;
|
||||
ctx.setDeckHoverPairs([p.aMmsi, p.bMmsi]);
|
||||
ctx.setDeckHoverMmsi([p.aMmsi, p.bMmsi]);
|
||||
ctx.clearMapFleetHoverState();
|
||||
},
|
||||
onClick: (info) => {
|
||||
if (!info.object) { ctx.onSelectMmsi(null); return; }
|
||||
const obj = info.object as PairRangeCircle;
|
||||
const sourceEvent = (info as { srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null }).srcEvent;
|
||||
if (sourceEvent && ctx.hasAuxiliarySelectModifier(sourceEvent)) {
|
||||
ctx.onToggleHighlightMmsi?.(obj.aMmsi);
|
||||
ctx.onToggleHighlightMmsi?.(obj.bMmsi);
|
||||
return;
|
||||
}
|
||||
ctx.onDeckSelectOrHighlight({ mmsi: obj.aMmsi });
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/* ─ pair lines ─ */
|
||||
if (ctx.overlays.pairLines && (ctx.pairLinks?.length ?? 0) > 0) {
|
||||
layers.push(
|
||||
new LineLayer<PairLink>({
|
||||
id: 'pair-lines',
|
||||
data: ctx.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; }
|
||||
ctx.touchDeckHoverState(true);
|
||||
const obj = info.object as PairLink;
|
||||
ctx.setDeckHoverPairs([obj.aMmsi, obj.bMmsi]);
|
||||
ctx.setDeckHoverMmsi([obj.aMmsi, obj.bMmsi]);
|
||||
ctx.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 && ctx.hasAuxiliarySelectModifier(sourceEvent)) {
|
||||
ctx.onToggleHighlightMmsi?.(obj.aMmsi);
|
||||
ctx.onToggleHighlightMmsi?.(obj.bMmsi);
|
||||
return;
|
||||
}
|
||||
ctx.onDeckSelectOrHighlight({ mmsi: obj.aMmsi });
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/* ─ fc lines ─ */
|
||||
if (ctx.overlays.fcLines && ctx.fcDashed.length > 0) {
|
||||
layers.push(
|
||||
new LineLayer<DashSeg>({
|
||||
id: 'fc-lines',
|
||||
data: ctx.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; }
|
||||
ctx.touchDeckHoverState(true);
|
||||
const obj = info.object as DashSeg;
|
||||
if (obj.fromMmsi == null || obj.toMmsi == null) { clearDeckHover(); return; }
|
||||
ctx.setDeckHoverPairs([obj.fromMmsi, obj.toMmsi]);
|
||||
ctx.setDeckHoverMmsi([obj.fromMmsi, obj.toMmsi]);
|
||||
ctx.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 && ctx.hasAuxiliarySelectModifier(sourceEvent)) {
|
||||
ctx.onToggleHighlightMmsi?.(obj.fromMmsi);
|
||||
ctx.onToggleHighlightMmsi?.(obj.toMmsi);
|
||||
return;
|
||||
}
|
||||
ctx.onDeckSelectOrHighlight({ mmsi: obj.fromMmsi });
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/* ─ fleet circles ─ */
|
||||
if (ctx.overlays.fleetCircles && (ctx.fleetCircles?.length ?? 0) > 0) {
|
||||
layers.push(
|
||||
new ScatterplotLayer<FleetCircle>({
|
||||
id: 'fleet-circles',
|
||||
data: ctx.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; }
|
||||
ctx.touchDeckHoverState(true);
|
||||
const obj = info.object as FleetCircle;
|
||||
const list = ctx.toFleetMmsiList(obj.vesselMmsis);
|
||||
ctx.setMapFleetHoverState(obj.ownerKey || null, list);
|
||||
ctx.setDeckHoverMmsi(list);
|
||||
ctx.clearDeckHoverPairs();
|
||||
},
|
||||
onClick: (info) => {
|
||||
if (!info.object) return;
|
||||
const obj = info.object as FleetCircle;
|
||||
const list = ctx.toFleetMmsiList(obj.vesselMmsis);
|
||||
const sourceEvent = (info as { srcEvent?: { shiftKey?: boolean; ctrlKey?: boolean; metaKey?: boolean } | null }).srcEvent;
|
||||
if (sourceEvent && ctx.hasAuxiliarySelectModifier(sourceEvent)) {
|
||||
for (const mmsi of list) ctx.onToggleHighlightMmsi?.(mmsi);
|
||||
return;
|
||||
}
|
||||
const first = list[0];
|
||||
if (first != null) ctx.onDeckSelectOrHighlight({ mmsi: first });
|
||||
},
|
||||
}),
|
||||
);
|
||||
layers.push(
|
||||
new ScatterplotLayer<FleetCircle>({
|
||||
id: 'fleet-circles-fill',
|
||||
data: ctx.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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/* ─ ships ─ */
|
||||
if (ctx.showShips) {
|
||||
const shipOnHover = (info: PickingInfo) => {
|
||||
if (!info.object) { clearDeckHover(); return; }
|
||||
ctx.touchDeckHoverState(true);
|
||||
const obj = info.object as AisTarget;
|
||||
ctx.setDeckHoverMmsi([obj.mmsi]);
|
||||
ctx.clearDeckHoverPairs();
|
||||
ctx.clearMapFleetHoverState();
|
||||
};
|
||||
const shipOnClick = (info: PickingInfo) => {
|
||||
if (!info.object) { ctx.onSelectMmsi(null); return; }
|
||||
ctx.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<AisTarget>({
|
||||
id: 'ships-other',
|
||||
data: shipOtherData,
|
||||
pickable: true,
|
||||
billboard: false,
|
||||
parameters: overlayParams,
|
||||
iconAtlas: getCachedShipIcon(),
|
||||
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<AisTarget>({
|
||||
id: 'ships-overlay-other',
|
||||
data: shipOverlayOtherData,
|
||||
pickable: false,
|
||||
billboard: false,
|
||||
parameters: overlayParams,
|
||||
iconAtlas: getCachedShipIcon(),
|
||||
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 (ctx.selectedMmsi != null && d.mmsi === ctx.selectedMmsi) return FLAT_SHIP_ICON_SIZE_SELECTED;
|
||||
if (ctx.shipHighlightSet.has(d.mmsi)) return FLAT_SHIP_ICON_SIZE_HIGHLIGHTED;
|
||||
return 0;
|
||||
},
|
||||
getColor: (d) => getShipColor(d, ctx.selectedMmsi, null, ctx.shipHighlightSet),
|
||||
alphaCutoff: 0.05,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (ctx.legacyTargetsOrdered.length > 0) {
|
||||
layers.push(
|
||||
new ScatterplotLayer<AisTarget>({
|
||||
id: 'legacy-halo',
|
||||
data: ctx.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<AisTarget>({
|
||||
id: 'ships-target',
|
||||
data: shipTargetData,
|
||||
pickable: true,
|
||||
billboard: false,
|
||||
parameters: overlayParams,
|
||||
iconAtlas: getCachedShipIcon(),
|
||||
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, ctx.legacyHits?.get(d.mmsi)?.shipCode ?? null, EMPTY_MMSI_SET),
|
||||
onHover: shipOnHover,
|
||||
onClick: shipOnClick,
|
||||
alphaCutoff: 0.05,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* ─ interactive overlays ─ */
|
||||
if (ctx.pairRangesInteractive.length > 0) {
|
||||
layers.push(new ScatterplotLayer<PairRangeCircle>({ id: 'pair-range-overlay', data: ctx.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 (ctx.pairLinksInteractive.length > 0) {
|
||||
layers.push(new LineLayer<PairLink>({ id: 'pair-lines-overlay', data: ctx.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 (ctx.fcLinesInteractive.length > 0) {
|
||||
layers.push(new LineLayer<DashSeg>({ id: 'fc-lines-overlay', data: ctx.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 (ctx.fleetCirclesInteractive.length > 0) {
|
||||
layers.push(new ScatterplotLayer<FleetCircle>({ id: 'fleet-circles-overlay-fill', data: ctx.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<FleetCircle>({ id: 'fleet-circles-overlay', data: ctx.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 }));
|
||||
}
|
||||
|
||||
/* ─ legacy overlay (highlight/selected) ─ */
|
||||
if (ctx.showShips && ctx.legacyOverlayTargets.length > 0) {
|
||||
layers.push(new ScatterplotLayer<AisTarget>({ id: 'legacy-halo-overlay', data: ctx.legacyOverlayTargets, pickable: false, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'pixels', getRadius: (d) => { if (ctx.selectedMmsi && d.mmsi === ctx.selectedMmsi) return FLAT_LEGACY_HALO_RADIUS_SELECTED; return FLAT_LEGACY_HALO_RADIUS_HIGHLIGHTED; }, lineWidthUnits: 'pixels', getLineWidth: (d) => (ctx.selectedMmsi && d.mmsi === ctx.selectedMmsi ? 2.5 : 2.2), getLineColor: (d) => { if (ctx.selectedMmsi && d.mmsi === ctx.selectedMmsi) return HALO_OUTLINE_COLOR_SELECTED; if (ctx.shipHighlightSet.has(d.mmsi)) return HALO_OUTLINE_COLOR_HIGHLIGHTED; return HALO_OUTLINE_COLOR; }, getPosition: (d) => [d.lon, d.lat] as [number, number] }));
|
||||
}
|
||||
|
||||
if (ctx.showShips && ctx.shipOverlayLayerData.filter((t) => ctx.legacyHits?.has(t.mmsi)).length > 0) {
|
||||
const shipOverlayTargetData2 = ctx.shipOverlayLayerData.filter((t) => ctx.legacyHits?.has(t.mmsi));
|
||||
layers.push(new IconLayer<AisTarget>({ id: 'ships-overlay-target', data: shipOverlayTargetData2, pickable: false, billboard: false, parameters: overlayParams, iconAtlas: getCachedShipIcon(), 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 (ctx.selectedMmsi != null && d.mmsi === ctx.selectedMmsi) return FLAT_SHIP_ICON_SIZE_SELECTED; if (ctx.shipHighlightSet.has(d.mmsi)) return FLAT_SHIP_ICON_SIZE_HIGHLIGHTED; return 0; }, getColor: (d) => { if (!ctx.shipHighlightSet.has(d.mmsi) && !(ctx.selectedMmsi != null && d.mmsi === ctx.selectedMmsi)) return [0, 0, 0, 0]; return getShipColor(d, ctx.selectedMmsi, ctx.legacyHits?.get(d.mmsi)?.shipCode ?? null, ctx.shipHighlightSet); } }));
|
||||
}
|
||||
|
||||
return layers;
|
||||
}
|
||||
|
||||
/* ── Globe Deck 오버레이 레이어 ────────────────────────── */
|
||||
|
||||
export interface GlobeDeckLayerContext {
|
||||
pairRanges: PairRangeCircle[];
|
||||
pairLinks: PairLink[] | undefined;
|
||||
fcDashed: DashSeg[];
|
||||
fleetCircles: FleetCircle[] | undefined;
|
||||
legacyTargetsOrdered: AisTarget[];
|
||||
legacyHits: Map<number, LegacyVesselInfo> | null | undefined;
|
||||
overlays: MapToggleState;
|
||||
showShips: boolean;
|
||||
selectedMmsi: number | null;
|
||||
isHighlightedFleet: (ownerKey: string, vesselMmsis: number[]) => boolean;
|
||||
isHighlightedPair: (aMmsi: number, bMmsi: number) => boolean;
|
||||
isHighlightedMmsi: (mmsi: number) => boolean;
|
||||
touchDeckHoverState: (isHover: boolean) => void;
|
||||
setDeckHoverPairs: (next: number[]) => void;
|
||||
setDeckHoverMmsi: (next: number[]) => void;
|
||||
clearDeckHoverPairs: () => void;
|
||||
clearDeckHoverMmsi: () => void;
|
||||
clearMapFleetHoverState: () => void;
|
||||
setMapFleetHoverState: (ownerKey: string | null, vesselMmsis: number[]) => void;
|
||||
toFleetMmsiList: (value: unknown) => number[];
|
||||
}
|
||||
|
||||
export function buildGlobeDeckLayers(ctx: GlobeDeckLayerContext): unknown[] {
|
||||
const overlayParams = GLOBE_OVERLAY_PARAMS;
|
||||
const globeLayers: unknown[] = [];
|
||||
|
||||
if (ctx.overlays.pairRange && ctx.pairRanges.length > 0) {
|
||||
globeLayers.push(new ScatterplotLayer<PairRangeCircle>({ id: 'pair-range-globe', data: ctx.pairRanges, pickable: true, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'meters', getRadius: (d) => d.radiusNm * 1852, radiusMinPixels: 10, lineWidthUnits: 'pixels', getLineWidth: (d) => (ctx.isHighlightedPair(d.aMmsi, d.bMmsi) ? 2.2 : 1), getLineColor: (d) => { const hl = ctx.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) { ctx.clearDeckHoverPairs(); ctx.clearDeckHoverMmsi(); return; } ctx.touchDeckHoverState(true); const p = info.object as PairRangeCircle; ctx.setDeckHoverPairs([p.aMmsi, p.bMmsi]); ctx.setDeckHoverMmsi([p.aMmsi, p.bMmsi]); ctx.clearMapFleetHoverState(); } }));
|
||||
}
|
||||
|
||||
if (ctx.overlays.pairLines && (ctx.pairLinks?.length ?? 0) > 0) {
|
||||
const links = ctx.pairLinks || [];
|
||||
globeLayers.push(new LineLayer<PairLink>({ id: 'pair-lines-globe', data: links, pickable: true, parameters: overlayParams, getSourcePosition: (d) => d.from, getTargetPosition: (d) => d.to, getColor: (d) => { const hl = ctx.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) => (ctx.isHighlightedPair(d.aMmsi, d.bMmsi) ? 2.6 : d.warn ? 2.2 : 1.4), widthUnits: 'pixels', onHover: (info) => { if (!info.object) { ctx.clearDeckHoverPairs(); ctx.clearDeckHoverMmsi(); return; } ctx.touchDeckHoverState(true); const obj = info.object as PairLink; ctx.setDeckHoverPairs([obj.aMmsi, obj.bMmsi]); ctx.setDeckHoverMmsi([obj.aMmsi, obj.bMmsi]); ctx.clearMapFleetHoverState(); } }));
|
||||
}
|
||||
|
||||
if (ctx.overlays.fcLines && ctx.fcDashed.length > 0) {
|
||||
globeLayers.push(new LineLayer<DashSeg>({ id: 'fc-lines-globe', data: ctx.fcDashed, pickable: true, parameters: overlayParams, getSourcePosition: (d) => d.from, getTargetPosition: (d) => d.to, getColor: (d) => { const ih = [d.fromMmsi, d.toMmsi].some((v) => ctx.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) => ctx.isHighlightedMmsi(v ?? -1)); return ih ? 1.9 : 1.3; }, widthUnits: 'pixels', onHover: (info) => { if (!info.object) { ctx.clearDeckHoverPairs(); ctx.clearDeckHoverMmsi(); return; } ctx.touchDeckHoverState(true); const obj = info.object as DashSeg; if (obj.fromMmsi == null || obj.toMmsi == null) { ctx.clearDeckHoverPairs(); ctx.clearDeckHoverMmsi(); return; } ctx.setDeckHoverPairs([obj.fromMmsi, obj.toMmsi]); ctx.setDeckHoverMmsi([obj.fromMmsi, obj.toMmsi]); ctx.clearMapFleetHoverState(); } }));
|
||||
}
|
||||
|
||||
if (ctx.overlays.fleetCircles && (ctx.fleetCircles?.length ?? 0) > 0) {
|
||||
const circles = ctx.fleetCircles || [];
|
||||
globeLayers.push(new ScatterplotLayer<FleetCircle>({ 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) => (ctx.isHighlightedFleet(d.ownerKey, d.vesselMmsis) ? 1.8 : 1.1), getLineColor: (d) => (ctx.isHighlightedFleet(d.ownerKey, d.vesselMmsis) ? FLEET_RANGE_LINE_DECK_HL : FLEET_RANGE_LINE_DECK), getPosition: (d) => d.center, onHover: (info) => { if (!info.object) { ctx.clearDeckHoverPairs(); ctx.clearDeckHoverMmsi(); ctx.clearMapFleetHoverState(); return; } ctx.touchDeckHoverState(true); const obj = info.object as FleetCircle; const list = ctx.toFleetMmsiList(obj.vesselMmsis); ctx.setMapFleetHoverState(obj.ownerKey || null, list); ctx.setDeckHoverMmsi(list); ctx.clearDeckHoverPairs(); } }));
|
||||
globeLayers.push(new ScatterplotLayer<FleetCircle>({ 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) => (ctx.isHighlightedFleet(d.ownerKey, d.vesselMmsis) ? FLEET_RANGE_FILL_DECK_HL : FLEET_RANGE_FILL_DECK), getPosition: (d) => d.center }));
|
||||
}
|
||||
|
||||
if (ctx.showShips && ctx.legacyTargetsOrdered.length > 0) {
|
||||
globeLayers.push(new ScatterplotLayer<AisTarget>({ id: 'legacy-halo-globe', data: ctx.legacyTargetsOrdered, pickable: false, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'pixels', getRadius: (d) => { if (ctx.selectedMmsi && d.mmsi === ctx.selectedMmsi) return FLAT_LEGACY_HALO_RADIUS_SELECTED; return FLAT_LEGACY_HALO_RADIUS; }, lineWidthUnits: 'pixels', getLineWidth: (d) => (ctx.selectedMmsi && d.mmsi === ctx.selectedMmsi ? 2.5 : 2), getLineColor: (d) => { if (ctx.selectedMmsi && d.mmsi === ctx.selectedMmsi) return HALO_OUTLINE_COLOR_SELECTED; return HALO_OUTLINE_COLOR; }, getPosition: (d) => [d.lon, d.lat] as [number, number] }));
|
||||
}
|
||||
|
||||
return globeLayers;
|
||||
}
|
||||
불러오는 중...
Reference in New Issue
Block a user