Map3D.tsx 단일 파일(5752줄)에서 1200줄을 16개 모듈로 추출하여 탐색성과 유지보수성 개선. 모듈 구조: - types.ts, constants.ts: 타입/상수 정의 - lib/: setUtils, geometry, featureIds, mlExpressions, shipUtils, tooltips, globeShipIcon, mapCore, dashifyLine, layerHelpers, zoneUtils - layers/: bathymetry, seamark - hooks/: useHoverState 버그 수정: - fix: Globe 선박 라벨 미표시 (permitted boolean→number + filter 갱신) - fix: placement TypeError (isStyleLoaded 가드 + epoch change 시 remove 제거) - fix: Globe easeTo 미지원 경고 (globe 모드에서 flyTo 사용) - fix: 수심지도 얕은 구간 색상 미구분 (색상 팔레트 개선) 개선: - 베이스맵 water 레이어 색상을 수심 그라데이션과 자연스럽게 연결 - 프로젝션 전환 settle 로직 최적화 (더블프레임→싱글프레임) - glyphs URL 추가로 symbol 레이어 텍스트 렌더링 지원 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
170 lines
5.6 KiB
TypeScript
170 lines
5.6 KiB
TypeScript
import type { AisTarget } from '../../../entities/aisTarget/model/types';
|
|
import type { LegacyVesselInfo } from '../../../entities/legacyVessel/model/types';
|
|
import { isFiniteNumber, toSafeNumber } from './setUtils';
|
|
|
|
export function formatNm(value: number | null | undefined) {
|
|
if (!isFiniteNumber(value)) return '-';
|
|
return `${value.toFixed(2)} NM`;
|
|
}
|
|
|
|
export function getLegacyTag(legacyHits: Map<number, LegacyVesselInfo> | null | undefined, mmsi: number) {
|
|
const legacy = legacyHits?.get(mmsi);
|
|
if (!legacy) return null;
|
|
return `${legacy.permitNo} (${legacy.shipCode})`;
|
|
}
|
|
|
|
export function getTargetName(
|
|
mmsi: number,
|
|
targetByMmsi: Map<number, AisTarget>,
|
|
legacyHits: Map<number, LegacyVesselInfo> | null | undefined,
|
|
) {
|
|
const legacy = legacyHits?.get(mmsi);
|
|
const target = targetByMmsi.get(mmsi);
|
|
return (
|
|
(target?.name || '').trim() || legacy?.shipNameCn || legacy?.shipNameRoman || `MMSI ${mmsi}`
|
|
);
|
|
}
|
|
|
|
export function getShipTooltipHtml({
|
|
mmsi,
|
|
targetByMmsi,
|
|
legacyHits,
|
|
}: {
|
|
mmsi: number;
|
|
targetByMmsi: Map<number, AisTarget>;
|
|
legacyHits: Map<number, LegacyVesselInfo> | null | undefined;
|
|
}) {
|
|
const legacy = legacyHits?.get(mmsi);
|
|
const t = targetByMmsi.get(mmsi);
|
|
const name = getTargetName(mmsi, targetByMmsi, legacyHits);
|
|
const sog = isFiniteNumber(t?.sog) ? t.sog : null;
|
|
const cog = isFiniteNumber(t?.cog) ? t.cog : null;
|
|
const msg = t?.messageTimestamp ?? null;
|
|
const vesselType = t?.vesselType || '';
|
|
|
|
const legacyHtml = legacy
|
|
? `<div style="margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,.08)">
|
|
<div><b>CN Permit</b> · <b>${legacy.shipCode}</b> · ${legacy.permitNo}</div>
|
|
<div>유효범위: ${legacy.workSeaArea || '-'}</div>
|
|
</div>`
|
|
: '';
|
|
|
|
return {
|
|
html: `<div style="font-family: system-ui; font-size: 12px; white-space: nowrap;">
|
|
<div style="font-weight: 700; margin-bottom: 4px;">${name}</div>
|
|
<div>MMSI: <b>${mmsi}</b>${vesselType ? ` · ${vesselType}` : ''}</div>
|
|
<div>SOG: <b>${sog ?? '?'}</b> kt · COG: <b>${cog ?? '?'}</b>°</div>
|
|
${msg ? `<div style="opacity:.7; font-size: 11px; margin-top: 4px;">${msg}</div>` : ''}
|
|
${legacyHtml}
|
|
</div>`,
|
|
};
|
|
}
|
|
|
|
export function getPairLinkTooltipHtml({
|
|
warn,
|
|
distanceNm,
|
|
aMmsi,
|
|
bMmsi,
|
|
legacyHits,
|
|
targetByMmsi,
|
|
}: {
|
|
warn: boolean;
|
|
distanceNm: number | null | undefined;
|
|
aMmsi: number;
|
|
bMmsi: number;
|
|
legacyHits: Map<number, LegacyVesselInfo> | null | undefined;
|
|
targetByMmsi: Map<number, AisTarget>;
|
|
}) {
|
|
const d = formatNm(distanceNm);
|
|
const a = getTargetName(aMmsi, targetByMmsi, legacyHits);
|
|
const b = getTargetName(bMmsi, targetByMmsi, legacyHits);
|
|
const aTag = getLegacyTag(legacyHits, aMmsi);
|
|
const bTag = getLegacyTag(legacyHits, bMmsi);
|
|
return {
|
|
html: `<div style="font-family: system-ui; font-size: 12px;">
|
|
<div style="font-weight: 700; margin-bottom: 4px;">쌍 연결</div>
|
|
<div>${aTag ?? `MMSI ${aMmsi}`}</div>
|
|
<div style="opacity:.85;">↔ ${bTag ?? `MMSI ${bMmsi}`}</div>
|
|
<div style="margin-top: 4px;">거리: <b>${d}</b> · 상태: <b>${warn ? '주의' : '정상'}</b></div>
|
|
<div style="opacity:.6; margin-top: 3px;">${a} / ${b}</div>
|
|
</div>`,
|
|
};
|
|
}
|
|
|
|
export function getFcLinkTooltipHtml({
|
|
suspicious,
|
|
distanceNm,
|
|
fcMmsi,
|
|
otherMmsi,
|
|
legacyHits,
|
|
targetByMmsi,
|
|
}: {
|
|
suspicious: boolean;
|
|
distanceNm: number | null | undefined;
|
|
fcMmsi: number;
|
|
otherMmsi: number;
|
|
legacyHits: Map<number, LegacyVesselInfo> | null | undefined;
|
|
targetByMmsi: Map<number, AisTarget>;
|
|
}) {
|
|
const d = formatNm(distanceNm);
|
|
const a = getTargetName(fcMmsi, targetByMmsi, legacyHits);
|
|
const b = getTargetName(otherMmsi, targetByMmsi, legacyHits);
|
|
const aTag = getLegacyTag(legacyHits, fcMmsi);
|
|
const bTag = getLegacyTag(legacyHits, otherMmsi);
|
|
return {
|
|
html: `<div style="font-family: system-ui; font-size: 12px;">
|
|
<div style="font-weight: 700; margin-bottom: 4px;">환적 연결</div>
|
|
<div>${aTag ?? `MMSI ${fcMmsi}`}</div>
|
|
<div style="opacity:.85;">→ ${bTag ?? `MMSI ${otherMmsi}`}</div>
|
|
<div style="margin-top: 4px;">거리: <b>${d}</b> · 상태: <b>${suspicious ? '의심' : '일반'}</b></div>
|
|
<div style="opacity:.6; margin-top: 3px;">${a} / ${b}</div>
|
|
</div>`,
|
|
};
|
|
}
|
|
|
|
export function getRangeTooltipHtml({
|
|
warn,
|
|
distanceNm,
|
|
aMmsi,
|
|
bMmsi,
|
|
legacyHits,
|
|
}: {
|
|
warn: boolean;
|
|
distanceNm: number | null | undefined;
|
|
aMmsi: number;
|
|
bMmsi: number;
|
|
legacyHits: Map<number, LegacyVesselInfo> | null | undefined;
|
|
}) {
|
|
const d = formatNm(distanceNm);
|
|
const aTag = getLegacyTag(legacyHits, aMmsi);
|
|
const bTag = getLegacyTag(legacyHits, bMmsi);
|
|
const radiusNm = toSafeNumber(distanceNm);
|
|
return {
|
|
html: `<div style="font-family: system-ui; font-size: 12px;">
|
|
<div style="font-weight: 700; margin-bottom: 4px;">쌍 연결범위</div>
|
|
<div>${aTag ?? `MMSI ${aMmsi}`}</div>
|
|
<div style="opacity:.85;">↔ ${bTag ?? `MMSI ${bMmsi}`}</div>
|
|
<div style="margin-top: 4px;">범위: <b>${d}</b> · 반경: <b>${formatNm(radiusNm == null ? null : radiusNm / 2)}</b> · 상태: <b>${warn ? '주의' : '정상'}</b></div>
|
|
</div>`,
|
|
};
|
|
}
|
|
|
|
export function getFleetCircleTooltipHtml({
|
|
ownerKey,
|
|
ownerLabel,
|
|
count,
|
|
}: {
|
|
ownerKey: string;
|
|
ownerLabel?: string;
|
|
count: number;
|
|
}) {
|
|
const displayOwner = ownerLabel && ownerLabel.trim() ? ownerLabel : ownerKey;
|
|
return {
|
|
html: `<div style="font-family: system-ui; font-size: 12px;">
|
|
<div style="font-weight: 700; margin-bottom: 4px;">선단 범위</div>
|
|
<div>소유주: ${displayOwner || '-'}</div>
|
|
<div>선박 수: <b>${count}</b></div>
|
|
</div>`,
|
|
};
|
|
}
|