feat(shipIcon): 선종별 SVG 아이콘 시스템 도입 + 대상 선박 브리딩 링
gc-wing-simple의 SVG 기반 선종별 아이콘 시스템을 도입하여 기타 AIS 선박을 8종 선종별 색상+형태(이동:화살표/정지:원형)로 구분하고, 대상 선박에는 legacy code 색상 + 브리딩 링 강조 효과를 적용한다. - shipKind.ts: 선종별 SVG 생성기 + 아이콘 스펙 사전 생성 - Mercator: 기타 AIS 20px SVG IconLayer, 대상 선박 26px SVG IconLayer - Globe: signalKindCode 기반 색상, 대상 선박 1.3x 크기 - 브리딩 rAF: 시안(선택)/주황(강조) 링, 2000ms 주기 - 범례: "기타 AIS(선종)" 7항목으로 변경 - shipIconCache.ts, SHIP_ICON_MAPPING 삭제 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
5c5af7e856
커밋
81fb4a2bca
@ -1,18 +1,4 @@
|
||||
// ── Shared map constants ──
|
||||
// Moved from widgets/map3d/constants.ts to resolve FSD layer violation
|
||||
// (features/ must not import from widgets/).
|
||||
|
||||
export const SHIP_ICON_MAPPING = {
|
||||
ship: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 128,
|
||||
height: 128,
|
||||
anchorX: 64,
|
||||
anchorY: 64,
|
||||
mask: true,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const DEPTH_DISABLED_PARAMS = {
|
||||
depthCompare: 'always',
|
||||
|
||||
187
apps/web/src/shared/lib/map/shipKind.ts
Normal file
187
apps/web/src/shared/lib/map/shipKind.ts
Normal file
@ -0,0 +1,187 @@
|
||||
// ── 선종(Ship Kind) 상수 + SVG 아이콘 생성 ──
|
||||
// gc-wing-simple의 SVG 기반 선종별 아이콘 시스템을 도입.
|
||||
// 기타 AIS: 선종별 색상 + 이동(화살표)/정지(원형) 분리.
|
||||
// 대상 선박: legacy code 색상 + 약간 더 큰 SVG + 흰색 테두리.
|
||||
|
||||
import { LEGACY_CODE_COLORS_RGB, rgbToHex, type Rgb } from './palette';
|
||||
|
||||
// ── 선종 상수 (8종) ──
|
||||
|
||||
export const SIGNAL_KIND = {
|
||||
FISHING: '000020',
|
||||
KCGV: '000021',
|
||||
PASSENGER: '000022',
|
||||
CARGO: '000023',
|
||||
TANKER: '000024',
|
||||
GOV: '000025',
|
||||
NORMAL: '000027',
|
||||
BUOY: '000028',
|
||||
} as const;
|
||||
|
||||
export type SignalKindCode = (typeof SIGNAL_KIND)[keyof typeof SIGNAL_KIND] | string;
|
||||
|
||||
/** 선종별 한글 라벨 */
|
||||
export const SHIP_KIND_LABELS: Record<string, string> = {
|
||||
'000020': '어선',
|
||||
'000021': '경비함정',
|
||||
'000022': '여객선',
|
||||
'000023': '화물선',
|
||||
'000024': '유조선',
|
||||
'000025': '관공선',
|
||||
'000027': '일반',
|
||||
'000028': '부이',
|
||||
};
|
||||
|
||||
/** 선종별 범례/UI 색상 (hex) */
|
||||
export const SHIP_KIND_COLORS: Record<string, string> = {
|
||||
'000020': '#00C853',
|
||||
'000021': '#FF5722',
|
||||
'000022': '#2196F3',
|
||||
'000023': '#9C27B0',
|
||||
'000024': '#F44336',
|
||||
'000025': '#FF9800',
|
||||
'000027': '#607D8B',
|
||||
'000028': '#795548',
|
||||
};
|
||||
|
||||
/** 정렬된 선종 코드 목록 (범례 표시 순서) */
|
||||
export const SHIP_KIND_ORDER = [
|
||||
'000020', '000021', '000022', '000023',
|
||||
'000024', '000025', '000027', '000028',
|
||||
] as const;
|
||||
|
||||
// ── SVG 아이콘 생성기 ──
|
||||
|
||||
const STROKE = 'rgba(0,0,0,0.6)';
|
||||
const TARGET_STROKE = 'rgba(255,255,255,0.7)';
|
||||
|
||||
/** 이동 중 선박 SVG (화살표 형태, 32×48) */
|
||||
export function makeMovingShipSvg(fill: string): string {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="48" viewBox="0 0 32 48"><path d="M16 2 L8 13 L4 28 L7 45 L25 45 L28 28 L24 13 Z" fill="${fill}" stroke="${STROKE}" stroke-width="1.5" stroke-linejoin="round"/></svg>`;
|
||||
}
|
||||
|
||||
/** 정지 선박 SVG (원형, 16×16) */
|
||||
export function makeStoppedShipSvg(fill: string): string {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><circle cx="8" cy="8" r="5.5" fill="${fill}" stroke="${STROKE}" stroke-width="1.2"/></svg>`;
|
||||
}
|
||||
|
||||
/** 부이 SVG (다색, 32×44) */
|
||||
export function makeBuoySvg(): string {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="44" viewBox="0 0 32 44"><line x1="16" y1="10" x2="16" y2="38" stroke="#5D4037" stroke-width="2.5" stroke-linecap="round"/><line x1="10" y1="38" x2="22" y2="38" stroke="#5D4037" stroke-width="2" stroke-linecap="round"/><ellipse cx="16" cy="24" rx="9" ry="7" fill="#E53935" stroke="#333" stroke-width="1"/><rect x="8" y="22" width="16" height="4" rx="1" fill="#FDD835" opacity="0.85"/><rect x="14.5" y="8" width="3" height="10" fill="#666"/><circle cx="16" cy="7" r="3.5" fill="#FFC107" stroke="#444" stroke-width="0.8"/></svg>`;
|
||||
}
|
||||
|
||||
/** 대상 선박 이동 SVG (36×52, 흰색 반투명 테두리) */
|
||||
export function makeTargetMovingShipSvg(fill: string): string {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="52" viewBox="0 0 36 52"><path d="M18 2 L9 14 L5 30 L8 49 L28 49 L31 30 L27 14 Z" fill="${fill}" stroke="${TARGET_STROKE}" stroke-width="2" stroke-linejoin="round"/></svg>`;
|
||||
}
|
||||
|
||||
/** 대상 선박 정지 SVG (20×20, 흰색 반투명 테두리) */
|
||||
export function makeTargetStoppedShipSvg(fill: string): string {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7" fill="${fill}" stroke="${TARGET_STROKE}" stroke-width="1.5"/></svg>`;
|
||||
}
|
||||
|
||||
function toDataUri(svg: string): string {
|
||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
/** Deck.gl IconLayer getIcon 반환 타입 */
|
||||
export interface ShipIconSpec {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
anchorX: number;
|
||||
anchorY: number;
|
||||
}
|
||||
|
||||
// ── 기타 AIS 아이콘 스펙 사전 생성 (8종 × 2상태 + buoy) ──
|
||||
|
||||
const OTHER_ICON_SPECS: Record<string, ShipIconSpec> = {};
|
||||
|
||||
for (const code of SHIP_KIND_ORDER) {
|
||||
const color = SHIP_KIND_COLORS[code] || '#607D8B';
|
||||
|
||||
if (code === '000028') {
|
||||
OTHER_ICON_SPECS[`${code}-buoy`] = {
|
||||
url: toDataUri(makeBuoySvg()),
|
||||
width: 32, height: 44, anchorX: 16, anchorY: 22,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
OTHER_ICON_SPECS[`${code}-moving`] = {
|
||||
url: toDataUri(makeMovingShipSvg(color)),
|
||||
width: 32, height: 48, anchorX: 16, anchorY: 24,
|
||||
};
|
||||
OTHER_ICON_SPECS[`${code}-stopped`] = {
|
||||
url: toDataUri(makeStoppedShipSvg(color)),
|
||||
width: 16, height: 16, anchorX: 8, anchorY: 8,
|
||||
};
|
||||
}
|
||||
|
||||
// fallback
|
||||
const FALLBACK_MOVING: ShipIconSpec = OTHER_ICON_SPECS['000027-moving'];
|
||||
const FALLBACK_STOPPED: ShipIconSpec = OTHER_ICON_SPECS['000027-stopped'];
|
||||
|
||||
// ── 대상 선박 아이콘 스펙 사전 생성 (7 legacyCode × 2상태) ──
|
||||
|
||||
const LEGACY_CODES = ['PT', 'PT-S', 'GN', 'OT', 'PS', 'FC', 'C21'] as const;
|
||||
|
||||
const TARGET_ICON_SPECS: Record<string, ShipIconSpec> = {};
|
||||
|
||||
for (const code of LEGACY_CODES) {
|
||||
const rgb: Rgb = LEGACY_CODE_COLORS_RGB[code] || [100, 116, 139];
|
||||
const hex = rgbToHex(rgb);
|
||||
|
||||
TARGET_ICON_SPECS[`${code}-moving`] = {
|
||||
url: toDataUri(makeTargetMovingShipSvg(hex)),
|
||||
width: 36, height: 52, anchorX: 18, anchorY: 26,
|
||||
};
|
||||
TARGET_ICON_SPECS[`${code}-stopped`] = {
|
||||
url: toDataUri(makeTargetStoppedShipSvg(hex)),
|
||||
width: 20, height: 20, anchorX: 10, anchorY: 10,
|
||||
};
|
||||
}
|
||||
|
||||
// fallback (FC 색상)
|
||||
const TARGET_FALLBACK_MOVING: ShipIconSpec = TARGET_ICON_SPECS['FC-moving'];
|
||||
const TARGET_FALLBACK_STOPPED: ShipIconSpec = TARGET_ICON_SPECS['FC-stopped'];
|
||||
|
||||
// ── SOG 기준 이동/정지 판단 (kn) ──
|
||||
|
||||
export const SPEED_THRESHOLD_KN = 1;
|
||||
|
||||
// ── 조회 함수 ──
|
||||
|
||||
/** 기타 AIS 아이콘 스펙 조회 */
|
||||
export function getShipIconSpec(
|
||||
signalKindCode: string | undefined | null,
|
||||
sog: number | undefined | null,
|
||||
): ShipIconSpec {
|
||||
const code = signalKindCode || '000027';
|
||||
if (code === '000028') return OTHER_ICON_SPECS['000028-buoy'] || FALLBACK_STOPPED;
|
||||
|
||||
const isMoving = Number.isFinite(sog) && (sog as number) > SPEED_THRESHOLD_KN;
|
||||
const key = `${code}-${isMoving ? 'moving' : 'stopped'}`;
|
||||
return OTHER_ICON_SPECS[key] || (isMoving ? FALLBACK_MOVING : FALLBACK_STOPPED);
|
||||
}
|
||||
|
||||
/** 대상 선박 아이콘 스펙 조회 */
|
||||
export function getTargetShipIconSpec(
|
||||
legacyShipCode: string | undefined | null,
|
||||
sog: number | undefined | null,
|
||||
): ShipIconSpec {
|
||||
const code = legacyShipCode || 'FC';
|
||||
const isMoving = Number.isFinite(sog) && (sog as number) > SPEED_THRESHOLD_KN;
|
||||
const key = `${code}-${isMoving ? 'moving' : 'stopped'}`;
|
||||
return TARGET_ICON_SPECS[key] || (isMoving ? TARGET_FALLBACK_MOVING : TARGET_FALLBACK_STOPPED);
|
||||
}
|
||||
|
||||
/** 선박 아이콘 회전각 (부이는 0, 나머지는 -cog) */
|
||||
export function getShipIconAngle(
|
||||
signalKindCode: string | undefined | null,
|
||||
cog: number | undefined | null,
|
||||
): number {
|
||||
const code = signalKindCode || '000027';
|
||||
if (code === '000028') return 0;
|
||||
return -(Number.isFinite(cog) ? (cog as number) : 0);
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { ZONE_IDS, ZONE_META } from "../../entities/zone/model/meta";
|
||||
import { LEGACY_CODE_COLORS_HEX, OTHER_AIS_SPEED_HEX, OVERLAY_RGB, rgbToHex, rgba } from "../../shared/lib/map/palette";
|
||||
import { LEGACY_CODE_COLORS_HEX, OVERLAY_RGB, rgbToHex, rgba } from "../../shared/lib/map/palette";
|
||||
import { SHIP_KIND_ORDER, SHIP_KIND_LABELS, SHIP_KIND_COLORS } from "../../shared/lib/map/shipKind";
|
||||
|
||||
export function MapLegend() {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
@ -25,23 +26,13 @@ export function MapLegend() {
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="lt" style={{ marginTop: 8 }}>기타 AIS 선박(속도)</div>
|
||||
<div className="li">
|
||||
<div className="ls" style={{ background: OTHER_AIS_SPEED_HEX.fast, borderRadius: 999 }} />
|
||||
SOG ≥ 10 kt
|
||||
</div>
|
||||
<div className="li">
|
||||
<div className="ls" style={{ background: OTHER_AIS_SPEED_HEX.moving, borderRadius: 999 }} />
|
||||
1 ≤ SOG < 10 kt
|
||||
</div>
|
||||
<div className="li">
|
||||
<div className="ls" style={{ background: OTHER_AIS_SPEED_HEX.stopped, borderRadius: 999 }} />
|
||||
SOG < 1 kt
|
||||
</div>
|
||||
<div className="li">
|
||||
<div className="ls" style={{ background: OTHER_AIS_SPEED_HEX.moving, opacity: 0.55, borderRadius: 999 }} />
|
||||
SOG unknown
|
||||
</div>
|
||||
<div className="lt" style={{ marginTop: 8 }}>기타 AIS 선박(선종)</div>
|
||||
{SHIP_KIND_ORDER.filter((c) => c !== '000028').map((code) => (
|
||||
<div key={code} className="li">
|
||||
<div className="ls" style={{ background: SHIP_KIND_COLORS[code], borderRadius: 999 }} />
|
||||
{SHIP_KIND_LABELS[code]}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="lt" style={{ marginTop: 8 }}>CN Permit(업종)</div>
|
||||
<div className="li">
|
||||
|
||||
@ -624,7 +624,7 @@ export function Map3D({
|
||||
useDeckLayers(
|
||||
mapRef, overlayRef, globeDeckLayerRef, projectionBusyRef,
|
||||
{
|
||||
projection, settings, trackReplayDeckLayers: trackReplayRenderState.trackReplayDeckLayers, shipLayerData, shipOverlayLayerData, shipData,
|
||||
projection, settings, trackReplayDeckLayers: trackReplayRenderState.trackReplayDeckLayers, shipLayerData, shipData,
|
||||
legacyHits, pairLinks, fcLinks, fcDashed, fleetCircles, pairRanges,
|
||||
pairLinksInteractive, pairRangesInteractive, fcLinesInteractive, fleetCirclesInteractive,
|
||||
overlays, shipByMmsi, selectedMmsi, shipHighlightSet,
|
||||
|
||||
@ -14,10 +14,7 @@ const OVERLAY_FC_TRANSFER_RGB = OVERLAY_RGB.fcTransfer;
|
||||
const OVERLAY_FLEET_RANGE_RGB = OVERLAY_RGB.fleetRange;
|
||||
const OVERLAY_SUSPICIOUS_RGB = OVERLAY_RGB.suspicious;
|
||||
|
||||
// ── Ship icon mapping (Deck.gl IconLayer) ──
|
||||
// Canonical source: shared/lib/map/mapConstants.ts (re-exported for local usage)
|
||||
|
||||
export { SHIP_ICON_MAPPING } from '../../shared/lib/map/mapConstants';
|
||||
// Ship icon mapping removed — now using shipKind.ts SVG-based icons
|
||||
|
||||
// ── Ship constants ──
|
||||
|
||||
@ -47,14 +44,20 @@ export const GLOBE_OUTLINE_OTHER = 'rgba(160,175,195,0.55)';
|
||||
|
||||
// ── Flat map icon sizes ──
|
||||
|
||||
export const FLAT_SHIP_ICON_SIZE = 19;
|
||||
export const FLAT_SHIP_ICON_SIZE_SELECTED = 28;
|
||||
export const FLAT_SHIP_ICON_SIZE_HIGHLIGHTED = 25;
|
||||
export const FLAT_OTHER_SHIP_SIZE = 20;
|
||||
export const FLAT_TARGET_SHIP_SIZE = 26;
|
||||
export const FLAT_LEGACY_HALO_RADIUS = 14;
|
||||
export const FLAT_LEGACY_HALO_RADIUS_SELECTED = 18;
|
||||
export const FLAT_LEGACY_HALO_RADIUS_HIGHLIGHTED = 16;
|
||||
export const EMPTY_MMSI_SET = new Set<number>();
|
||||
|
||||
// ── 대상 선박 브리딩 애니메이션 ──
|
||||
|
||||
export const HALO_BREATHE_PERIOD_MS = 2000;
|
||||
export const HALO_BREATHE_SELECTED_R_MIN = 16;
|
||||
export const HALO_BREATHE_SELECTED_R_MAX = 22;
|
||||
export const HALO_BREATHE_HIGHLIGHTED_R_MIN = 14;
|
||||
export const HALO_BREATHE_HIGHLIGHTED_R_MAX = 19;
|
||||
|
||||
// ── Deck.gl view ID ──
|
||||
|
||||
export const DECK_VIEW_ID = 'mapbox';
|
||||
|
||||
@ -19,6 +19,13 @@ import {
|
||||
} from '../lib/tooltips';
|
||||
import { sanitizeDeckLayerList } from '../lib/mapCore';
|
||||
import { buildMercatorDeckLayers, buildGlobeDeckLayers } from '../lib/deckLayerFactories';
|
||||
import {
|
||||
HALO_BREATHE_PERIOD_MS,
|
||||
HALO_BREATHE_SELECTED_R_MIN,
|
||||
HALO_BREATHE_SELECTED_R_MAX,
|
||||
HALO_BREATHE_HIGHLIGHTED_R_MIN,
|
||||
HALO_BREATHE_HIGHLIGHTED_R_MAX,
|
||||
} from '../constants';
|
||||
// NOTE:
|
||||
// Globe mode now relies on MapLibre native overlays (useGlobeOverlays/useGlobeShips).
|
||||
// Keep Deck custom-layer rendering disabled in globe to avoid projection-space artifacts.
|
||||
@ -35,7 +42,6 @@ export function useDeckLayers(
|
||||
settings: Map3DSettings;
|
||||
trackReplayDeckLayers: unknown[];
|
||||
shipLayerData: AisTarget[];
|
||||
shipOverlayLayerData: AisTarget[];
|
||||
shipData: AisTarget[];
|
||||
legacyHits: Map<number, LegacyVesselInfo> | null | undefined;
|
||||
pairLinks: PairLink[] | undefined;
|
||||
@ -72,7 +78,7 @@ export function useDeckLayers(
|
||||
},
|
||||
) {
|
||||
const {
|
||||
projection, settings, trackReplayDeckLayers, shipLayerData, shipOverlayLayerData, shipData,
|
||||
projection, settings, trackReplayDeckLayers, shipLayerData, shipData,
|
||||
legacyHits, pairLinks, fcDashed, fleetCircles, pairRanges,
|
||||
pairLinksInteractive, pairRangesInteractive, fcLinesInteractive, fleetCirclesInteractive,
|
||||
overlays, shipByMmsi, selectedMmsi, shipHighlightSet,
|
||||
@ -98,9 +104,12 @@ export function useDeckLayers(
|
||||
}, [legacyTargets]);
|
||||
|
||||
const legacyOverlayTargets = useMemo(() => {
|
||||
if (shipHighlightSet.size === 0) return [];
|
||||
return legacyTargets.filter((target) => shipHighlightSet.has(target.mmsi));
|
||||
}, [legacyTargets, shipHighlightSet]);
|
||||
if (shipHighlightSet.size === 0 && selectedMmsi == null) return [];
|
||||
return legacyTargets.filter((target) =>
|
||||
shipHighlightSet.has(target.mmsi) ||
|
||||
(selectedMmsi != null && target.mmsi === selectedMmsi),
|
||||
);
|
||||
}, [legacyTargets, shipHighlightSet, selectedMmsi]);
|
||||
|
||||
const alarmTargets = useMemo(() => {
|
||||
if (!alarmMmsiMap || alarmMmsiMap.size === 0) return [];
|
||||
@ -134,7 +143,6 @@ export function useDeckLayers(
|
||||
|
||||
const layers = buildMercatorDeckLayers({
|
||||
shipLayerData,
|
||||
shipOverlayLayerData,
|
||||
legacyTargetsOrdered,
|
||||
legacyOverlayTargets,
|
||||
legacyHits,
|
||||
@ -246,7 +254,6 @@ export function useDeckLayers(
|
||||
legacyTargetsOrdered,
|
||||
legacyHits,
|
||||
legacyOverlayTargets,
|
||||
shipOverlayLayerData,
|
||||
pairRangesInteractive,
|
||||
pairLinksInteractive,
|
||||
fcLinesInteractive,
|
||||
@ -275,9 +282,12 @@ export function useDeckLayers(
|
||||
onClickShipPhoto,
|
||||
]);
|
||||
|
||||
// Mercator alarm pulse breathing animation (rAF)
|
||||
// Mercator 브리딩 애니메이션 (rAF) — 알람 맥동 + 대상 선박 브리딩 합류
|
||||
const hasAlarms = alarmMmsiMap && alarmMmsiMap.size > 0 && alarmTargets.length > 0;
|
||||
const hasTargetOverlays = legacyOverlayTargets.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (projection !== 'mercator' || !alarmMmsiMap || alarmMmsiMap.size === 0 || alarmTargets.length === 0) {
|
||||
if (projection !== 'mercator' || (!hasAlarms && !hasTargetOverlays)) {
|
||||
if (alarmRafRef.current) cancelAnimationFrame(alarmRafRef.current);
|
||||
alarmRafRef.current = 0;
|
||||
return;
|
||||
@ -295,34 +305,70 @@ export function useDeckLayers(
|
||||
return;
|
||||
}
|
||||
|
||||
const t = (Math.sin(Date.now() / 1500 * Math.PI * 2) + 1) / 2;
|
||||
const normalR = 8 + t * 6;
|
||||
const hoverR = 12 + t * 6;
|
||||
const now = Date.now();
|
||||
let updated = mercatorLayersRef.current;
|
||||
|
||||
const pulseLyr = new ScatterplotLayer<AisTarget>({
|
||||
id: 'alarm-pulse',
|
||||
data: alarmTargets,
|
||||
pickable: false,
|
||||
billboard: false,
|
||||
filled: true,
|
||||
stroked: false,
|
||||
radiusUnits: 'pixels',
|
||||
getRadius: (d) => {
|
||||
const isHover = (selectedMmsi != null && d.mmsi === selectedMmsi) || shipHighlightSet.has(d.mmsi);
|
||||
return isHover ? hoverR : normalR;
|
||||
},
|
||||
getFillColor: (d) => {
|
||||
const kind = alarmMmsiMap.get(d.mmsi);
|
||||
return kind ? ALARM_BADGE[kind].rgba : [107, 114, 128, 90] as [number, number, number, number];
|
||||
},
|
||||
getPosition: (d) => [d.lon, d.lat] as [number, number],
|
||||
updateTriggers: { getRadius: [normalR, hoverR] },
|
||||
});
|
||||
// 1. 알람 맥동 (기존)
|
||||
if (hasAlarms) {
|
||||
const tA = (Math.sin(now / 1500 * Math.PI * 2) + 1) / 2;
|
||||
const normalR = 8 + tA * 6;
|
||||
const hoverR = 12 + tA * 6;
|
||||
|
||||
const updated = mercatorLayersRef.current.map((l) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(l as any)?.id === 'alarm-pulse' ? pulseLyr : l,
|
||||
);
|
||||
const pulseLyr = new ScatterplotLayer<AisTarget>({
|
||||
id: 'alarm-pulse',
|
||||
data: alarmTargets,
|
||||
pickable: false,
|
||||
billboard: false,
|
||||
filled: true,
|
||||
stroked: false,
|
||||
radiusUnits: 'pixels',
|
||||
getRadius: (d) => {
|
||||
const isHover = (selectedMmsi != null && d.mmsi === selectedMmsi) || shipHighlightSet.has(d.mmsi);
|
||||
return isHover ? hoverR : normalR;
|
||||
},
|
||||
getFillColor: (d) => {
|
||||
const kind = alarmMmsiMap!.get(d.mmsi);
|
||||
return kind ? ALARM_BADGE[kind].rgba : [107, 114, 128, 90] as [number, number, number, number];
|
||||
},
|
||||
getPosition: (d) => [d.lon, d.lat] as [number, number],
|
||||
updateTriggers: { getRadius: [normalR, hoverR] },
|
||||
});
|
||||
updated = updated.map((l) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(l as any)?.id === 'alarm-pulse' ? pulseLyr : l,
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 대상 선박 브리딩 링
|
||||
if (hasTargetOverlays) {
|
||||
const tH = (Math.sin(now / HALO_BREATHE_PERIOD_MS * Math.PI * 2) + 1) / 2;
|
||||
const selR = HALO_BREATHE_SELECTED_R_MIN + tH * (HALO_BREATHE_SELECTED_R_MAX - HALO_BREATHE_SELECTED_R_MIN);
|
||||
const hlR = HALO_BREATHE_HIGHLIGHTED_R_MIN + tH * (HALO_BREATHE_HIGHLIGHTED_R_MAX - HALO_BREATHE_HIGHLIGHTED_R_MIN);
|
||||
const alpha = Math.round(155 + tH * 100);
|
||||
|
||||
const haloLyr = new ScatterplotLayer<AisTarget>({
|
||||
id: 'legacy-halo-overlay',
|
||||
data: legacyOverlayTargets,
|
||||
pickable: false,
|
||||
billboard: false,
|
||||
filled: false,
|
||||
stroked: true,
|
||||
radiusUnits: 'pixels',
|
||||
getRadius: (d) => (selectedMmsi != null && d.mmsi === selectedMmsi ? selR : hlR),
|
||||
lineWidthUnits: 'pixels',
|
||||
getLineWidth: 2.5,
|
||||
getLineColor: (d) => {
|
||||
if (selectedMmsi != null && d.mmsi === selectedMmsi) return [14, 234, 255, alpha] as [number, number, number, number];
|
||||
return [245, 158, 11, alpha] as [number, number, number, number];
|
||||
},
|
||||
getPosition: (d) => [d.lon, d.lat] as [number, number],
|
||||
updateTriggers: { getRadius: [selR, hlR], getLineColor: [alpha, selectedMmsi] },
|
||||
});
|
||||
updated = updated.map((l) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(l as any)?.id === 'legacy-halo-overlay' ? haloLyr : l,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
currentOverlay.setProps({ layers: updated } as never);
|
||||
@ -336,7 +382,7 @@ export function useDeckLayers(
|
||||
if (alarmRafRef.current) cancelAnimationFrame(alarmRafRef.current);
|
||||
alarmRafRef.current = 0;
|
||||
};
|
||||
}, [projection, alarmMmsiMap, alarmTargets, selectedMmsi, shipHighlightSet]);
|
||||
}, [projection, alarmMmsiMap, alarmTargets, selectedMmsi, shipHighlightSet, hasAlarms, hasTargetOverlays, legacyOverlayTargets]);
|
||||
|
||||
// Globe Deck overlay
|
||||
useEffect(() => {
|
||||
|
||||
@ -123,7 +123,7 @@ export function useGlobeShipHover(
|
||||
sog: isFiniteNumber(t.sog) ? t.sog : 0,
|
||||
shipColor: getGlobeBaseShipColor({
|
||||
legacy: legacy?.shipCode || null,
|
||||
sog: isFiniteNumber(t.sog) ? t.sog : null,
|
||||
signalKindCode: t.signalKindCode || t.shipKindCode || '000027',
|
||||
}),
|
||||
iconSize3: clampNumber(0.35 * sizeScale * scale, 0.25, 1.45),
|
||||
iconSize7: clampNumber(0.45 * sizeScale * scale, 0.3, 1.7),
|
||||
|
||||
@ -84,9 +84,9 @@ export function useGlobeShipLayers(
|
||||
(isFiniteNumber(t.length) ? t.length : 0) + (isFiniteNumber(t.width) ? t.width : 0) * 3,
|
||||
50, 420,
|
||||
);
|
||||
const sizeScale = clampNumber(0.85 + (hull - 50) / 420, 0.82, 1.85);
|
||||
// 상호작용 스케일링 제거 — selected/highlighted는 feature-state로 처리
|
||||
// hover overlay 레이어가 확대 + z-priority를 담당
|
||||
const baseSizeScale = clampNumber(0.85 + (hull - 50) / 420, 0.82, 1.85);
|
||||
// 대상 선박은 1.3x 배율 적용
|
||||
const sizeScale = legacy ? baseSizeScale * 1.3 : baseSizeScale;
|
||||
const iconSize3 = clampNumber(0.35 * sizeScale, 0.25, 1.3);
|
||||
const iconSize7 = clampNumber(0.45 * sizeScale, 0.3, 1.45);
|
||||
const iconSize10 = clampNumber(0.58 * sizeScale, 0.35, 1.8);
|
||||
@ -106,7 +106,7 @@ export function useGlobeShipLayers(
|
||||
isAnchored: isAnchored ? 1 : 0,
|
||||
shipColor: getGlobeBaseShipColor({
|
||||
legacy: legacy?.shipCode || null,
|
||||
sog: isFiniteNumber(t.sog) ? t.sog : null,
|
||||
signalKindCode: t.signalKindCode || t.shipKindCode || '000027',
|
||||
}),
|
||||
iconSize3,
|
||||
iconSize7,
|
||||
|
||||
@ -8,19 +8,14 @@ import type { FleetCircle, PairLink } from '../../../features/legacyDashboard/mo
|
||||
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_OTHER_SHIP_SIZE,
|
||||
FLAT_TARGET_SHIP_SIZE,
|
||||
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,
|
||||
@ -38,8 +33,13 @@ import {
|
||||
FLEET_RANGE_LINE_DECK_HL,
|
||||
FLEET_RANGE_FILL_DECK_HL,
|
||||
} from '../constants';
|
||||
import { getDisplayHeading, getShipColor } from './shipUtils';
|
||||
import { getCachedShipIcon } from './shipIconCache';
|
||||
import { getDisplayHeading } from './shipUtils';
|
||||
import {
|
||||
getShipIconSpec,
|
||||
getTargetShipIconSpec,
|
||||
getShipIconAngle,
|
||||
SPEED_THRESHOLD_KN,
|
||||
} from '../../../shared/lib/map/shipKind';
|
||||
|
||||
/* ── 공통 콜백 인터페이스 ─────────────────────────────── */
|
||||
|
||||
@ -64,7 +64,6 @@ interface DeckSelectCallbacks {
|
||||
|
||||
export interface MercatorDeckLayerContext extends DeckHoverCallbacks, DeckSelectCallbacks {
|
||||
shipLayerData: AisTarget[];
|
||||
shipOverlayLayerData: AisTarget[];
|
||||
legacyTargetsOrdered: AisTarget[];
|
||||
legacyOverlayTargets: AisTarget[];
|
||||
legacyHits: Map<number, LegacyVesselInfo> | null | undefined;
|
||||
@ -101,10 +100,6 @@ export function buildMercatorDeckLayers(ctx: MercatorDeckLayerContext): unknown[
|
||||
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) {
|
||||
@ -318,26 +313,6 @@ export function buildMercatorDeckLayers(ctx: MercatorDeckLayerContext): unknown[
|
||||
};
|
||||
|
||||
if (shipOtherData.length > 0) {
|
||||
layers.push(
|
||||
new ScatterplotLayer<AisTarget>({
|
||||
id: 'ships-other-halo',
|
||||
data: shipOtherData,
|
||||
pickable: false,
|
||||
billboard: false,
|
||||
parameters: overlayParams,
|
||||
getPosition: (d) => [d.lon, d.lat] as [number, number],
|
||||
radiusUnits: 'pixels',
|
||||
getRadius: 10,
|
||||
getFillColor: (d) => getShipColor(d, null, null, EMPTY_MMSI_SET).slice(0, 3).concat(40) as unknown as [number, number, number, number],
|
||||
getLineColor: (d) => {
|
||||
const c = getShipColor(d, null, null, EMPTY_MMSI_SET);
|
||||
return [c[0], c[1], c[2], 100] as [number, number, number, number];
|
||||
},
|
||||
stroked: true,
|
||||
lineWidthUnits: 'pixels',
|
||||
getLineWidth: 1,
|
||||
}),
|
||||
);
|
||||
layers.push(
|
||||
new IconLayer<AisTarget>({
|
||||
id: 'ships-other',
|
||||
@ -345,14 +320,11 @@ export function buildMercatorDeckLayers(ctx: MercatorDeckLayerContext): unknown[
|
||||
pickable: true,
|
||||
billboard: false,
|
||||
parameters: overlayParams,
|
||||
iconAtlas: getCachedShipIcon(),
|
||||
iconMapping: SHIP_ICON_MAPPING,
|
||||
getIcon: () => 'ship',
|
||||
getIcon: (d) => getShipIconSpec(d.signalKindCode ?? d.shipKindCode, d.sog),
|
||||
getPosition: (d) => [d.lon, d.lat] as [number, number],
|
||||
getAngle: (d) => -getDisplayHeading({ cog: d.cog, heading: d.heading }),
|
||||
getAngle: (d) => getShipIconAngle(d.signalKindCode ?? d.shipKindCode, d.cog),
|
||||
sizeUnits: 'pixels',
|
||||
getSize: () => FLAT_SHIP_ICON_SIZE,
|
||||
getColor: (d) => getShipColor(d, null, null, EMPTY_MMSI_SET),
|
||||
getSize: FLAT_OTHER_SHIP_SIZE,
|
||||
onHover: shipOnHover,
|
||||
onClick: shipOnClick,
|
||||
alphaCutoff: 0.05,
|
||||
@ -360,31 +332,6 @@ export function buildMercatorDeckLayers(ctx: MercatorDeckLayerContext): unknown[
|
||||
);
|
||||
}
|
||||
|
||||
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>({
|
||||
@ -413,14 +360,14 @@ export function buildMercatorDeckLayers(ctx: MercatorDeckLayerContext): unknown[
|
||||
pickable: true,
|
||||
billboard: false,
|
||||
parameters: overlayParams,
|
||||
iconAtlas: getCachedShipIcon(),
|
||||
iconMapping: SHIP_ICON_MAPPING,
|
||||
getIcon: () => 'ship',
|
||||
getIcon: (d) => getTargetShipIconSpec(ctx.legacyHits?.get(d.mmsi)?.shipCode ?? null, d.sog),
|
||||
getPosition: (d) => [d.lon, d.lat] as [number, number],
|
||||
getAngle: (d) => -getDisplayHeading({ cog: d.cog, heading: d.heading }),
|
||||
getAngle: (d) => {
|
||||
const isMoving = Number.isFinite(d.sog) && (d.sog as number) > SPEED_THRESHOLD_KN;
|
||||
return isMoving ? -getDisplayHeading({ cog: d.cog, heading: d.heading }) : 0;
|
||||
},
|
||||
sizeUnits: 'pixels',
|
||||
getSize: () => FLAT_SHIP_ICON_SIZE,
|
||||
getColor: (d) => getShipColor(d, null, ctx.legacyHits?.get(d.mmsi)?.shipCode ?? null, EMPTY_MMSI_SET),
|
||||
getSize: FLAT_TARGET_SHIP_SIZE,
|
||||
onHover: shipOnHover,
|
||||
onClick: shipOnClick,
|
||||
alphaCutoff: 0.05,
|
||||
@ -444,14 +391,26 @@ export function buildMercatorDeckLayers(ctx: MercatorDeckLayerContext): unknown[
|
||||
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: () => 3.0, getLineColor: () => FLEET_RANGE_LINE_DECK_HL, getPosition: (d) => d.center }));
|
||||
}
|
||||
|
||||
/* ─ legacy overlay (highlight/selected) ─ */
|
||||
/* ─ legacy overlay (highlight/selected — breathing ring, no icon enlargement) ─ */
|
||||
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); } }));
|
||||
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) => (ctx.selectedMmsi && d.mmsi === ctx.selectedMmsi ? 18 : 16),
|
||||
lineWidthUnits: 'pixels',
|
||||
getLineWidth: 2.5,
|
||||
getLineColor: (d) => {
|
||||
if (ctx.selectedMmsi && d.mmsi === ctx.selectedMmsi) return [14, 234, 255, 200];
|
||||
return [245, 158, 11, 190];
|
||||
},
|
||||
getPosition: (d) => [d.lon, d.lat] as [number, number],
|
||||
}));
|
||||
}
|
||||
|
||||
/* ─ ship name labels (Mercator) ─ */
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
/**
|
||||
* Ship SVG 아이콘을 미리 fetch하여 data URL로 캐시.
|
||||
* Deck.gl IconLayer가 매번 iconAtlas URL을 fetch하지 않도록
|
||||
* 인라인 data URL을 전달한다.
|
||||
*/
|
||||
const SHIP_SVG_URL = '/assets/ship.svg';
|
||||
|
||||
let _cachedDataUrl: string | null = null;
|
||||
let _promise: Promise<string> | null = null;
|
||||
|
||||
function preloadShipIcon(): Promise<string> {
|
||||
if (_cachedDataUrl) return Promise.resolve(_cachedDataUrl);
|
||||
if (_promise) return _promise;
|
||||
_promise = fetch(SHIP_SVG_URL)
|
||||
.then((res) => res.text())
|
||||
.then((svg) => {
|
||||
_cachedDataUrl = `data:image/svg+xml;base64,${btoa(svg)}`;
|
||||
return _cachedDataUrl;
|
||||
})
|
||||
.catch(() => SHIP_SVG_URL);
|
||||
return _promise;
|
||||
}
|
||||
|
||||
/** 캐시된 data URL 또는 폴백 URL 반환 */
|
||||
export function getCachedShipIcon(): string {
|
||||
return _cachedDataUrl ?? SHIP_SVG_URL;
|
||||
}
|
||||
|
||||
// 모듈 임포트 시 즉시 로드 시작
|
||||
preloadShipIcon();
|
||||
@ -1,12 +1,10 @@
|
||||
import type { AisTarget } from '../../../entities/aisTarget/model/types';
|
||||
import type { LegacyVesselInfo } from '../../../entities/legacyVessel/model/types';
|
||||
import { rgbToHex } from '../../../shared/lib/map/palette';
|
||||
import { SHIP_KIND_COLORS } from '../../../shared/lib/map/shipKind';
|
||||
import {
|
||||
ANCHOR_SPEED_THRESHOLD_KN,
|
||||
LEGACY_CODE_COLORS,
|
||||
MAP_SELECTED_SHIP_RGB,
|
||||
MAP_HIGHLIGHT_SHIP_RGB,
|
||||
MAP_DEFAULT_SHIP_RGB,
|
||||
} from '../constants';
|
||||
import { isFiniteNumber } from './setUtils';
|
||||
import { normalizeAngleDeg } from './geometry';
|
||||
@ -53,44 +51,21 @@ export function lightenColor(rgb: [number, number, number], ratio = 0.32) {
|
||||
|
||||
export function getGlobeBaseShipColor({
|
||||
legacy,
|
||||
sog,
|
||||
signalKindCode,
|
||||
}: {
|
||||
legacy: string | null;
|
||||
sog: number | null;
|
||||
signalKindCode?: string;
|
||||
}) {
|
||||
// 대상 선박: legacy code 색상 (밝게)
|
||||
if (legacy) {
|
||||
const rgb = LEGACY_CODE_COLORS[legacy];
|
||||
if (rgb) return rgbToHex(lightenColor(rgb, 0.38));
|
||||
}
|
||||
|
||||
// Keep alpha control in icon-opacity only to avoid double-multiplying transparency.
|
||||
if (!isFiniteNumber(sog)) return '#64748b';
|
||||
if (sog >= 10) return '#94a3b8';
|
||||
if (sog >= 1) return '#64748b';
|
||||
return '#475569';
|
||||
}
|
||||
|
||||
export function getShipColor(
|
||||
t: AisTarget,
|
||||
selectedMmsi: number | null,
|
||||
legacyShipCode: string | null,
|
||||
highlightedMmsis: Set<number>,
|
||||
): [number, number, number, number] {
|
||||
if (selectedMmsi && t.mmsi === selectedMmsi) {
|
||||
return [MAP_SELECTED_SHIP_RGB[0], MAP_SELECTED_SHIP_RGB[1], MAP_SELECTED_SHIP_RGB[2], 255];
|
||||
}
|
||||
if (highlightedMmsis.has(t.mmsi)) {
|
||||
return [MAP_HIGHLIGHT_SHIP_RGB[0], MAP_HIGHLIGHT_SHIP_RGB[1], MAP_HIGHLIGHT_SHIP_RGB[2], 235];
|
||||
}
|
||||
if (legacyShipCode) {
|
||||
const rgb = LEGACY_CODE_COLORS[legacyShipCode];
|
||||
if (rgb) return [rgb[0], rgb[1], rgb[2], 235];
|
||||
return [245, 158, 11, 235];
|
||||
}
|
||||
if (!isFiniteNumber(t.sog)) return [MAP_DEFAULT_SHIP_RGB[0], MAP_DEFAULT_SHIP_RGB[1], MAP_DEFAULT_SHIP_RGB[2], 175];
|
||||
if (t.sog >= 10) return [148, 163, 184, 215];
|
||||
if (t.sog >= 1) return [MAP_DEFAULT_SHIP_RGB[0], MAP_DEFAULT_SHIP_RGB[1], MAP_DEFAULT_SHIP_RGB[2], 210];
|
||||
return [71, 85, 105, 200];
|
||||
// 기타 AIS: signalKindCode → 선종별 색상
|
||||
const kindColor = SHIP_KIND_COLORS[signalKindCode || '000027'];
|
||||
if (kindColor) return kindColor;
|
||||
return '#607D8B';
|
||||
}
|
||||
|
||||
export function buildGlobeShipFeature(
|
||||
@ -108,7 +83,10 @@ export function buildGlobeShipFeature(
|
||||
mmsi: t.mmsi,
|
||||
heading: getDisplayHeading({ cog: t.cog, heading: t.heading, offset }),
|
||||
anchored,
|
||||
color: getGlobeBaseShipColor({ legacy: legacy?.shipCode ?? null, sog: t.sog ?? null }),
|
||||
color: getGlobeBaseShipColor({
|
||||
legacy: legacy?.shipCode ?? null,
|
||||
signalKindCode: t.signalKindCode || t.shipKindCode || '000027',
|
||||
}),
|
||||
selected: isSelected,
|
||||
highlighted: isHighlighted,
|
||||
permitted: legacy ? 1 : 0,
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user