gc-wing/apps/web/src/widgets/map3d/lib/shipUtils.ts
htlee 6acf2045b2 chore: vessel-track 브랜치 병합 (squash)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 22:12:48 +09:00

119 lines
3.7 KiB
TypeScript

import type { AisTarget } from '../../../entities/aisTarget/model/types';
import type { LegacyVesselInfo } from '../../../entities/legacyVessel/model/types';
import { rgbToHex } from '../../../shared/lib/map/palette';
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';
export function toValidBearingDeg(value: unknown): number | null {
if (typeof value !== 'number' || !Number.isFinite(value)) return null;
if (value === 511) return null;
if (value < 0) return null;
if (value >= 360) return null;
return value;
}
export function isAnchoredShip({
sog,
cog,
heading,
}: {
sog: number | null | undefined;
cog: number | null | undefined;
heading: number | null | undefined;
}): boolean {
if (!isFiniteNumber(sog)) return true;
if (sog <= ANCHOR_SPEED_THRESHOLD_KN) return true;
return toValidBearingDeg(cog) == null && toValidBearingDeg(heading) == null;
}
export function getDisplayHeading({
cog,
heading,
offset = 0,
}: {
cog: number | null | undefined;
heading: number | null | undefined;
offset?: number;
}) {
const raw = toValidBearingDeg(cog) ?? toValidBearingDeg(heading) ?? 0;
return normalizeAngleDeg(raw, offset);
}
export function lightenColor(rgb: [number, number, number], ratio = 0.32) {
const out = rgb.map((v) => Math.round(v + (255 - v) * ratio) as number) as [number, number, number];
return out;
}
export function getGlobeBaseShipColor({
legacy,
sog,
}: {
legacy: string | null;
sog: number | null;
}) {
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], 130];
if (t.sog >= 10) return [148, 163, 184, 185];
if (t.sog >= 1) return [MAP_DEFAULT_SHIP_RGB[0], MAP_DEFAULT_SHIP_RGB[1], MAP_DEFAULT_SHIP_RGB[2], 175];
return [71, 85, 105, 165];
}
export function buildGlobeShipFeature(
t: AisTarget,
legacy: LegacyVesselInfo | undefined,
selectedMmsi: number | null,
highlightedMmsis: Set<number>,
offset: number,
) {
const isSelected = selectedMmsi != null && t.mmsi === selectedMmsi ? 1 : 0;
const isHighlighted = highlightedMmsis.has(t.mmsi) ? 1 : 0;
const anchored = isAnchoredShip(t);
return {
mmsi: t.mmsi,
heading: getDisplayHeading({ cog: t.cog, heading: t.heading, offset }),
anchored,
color: getGlobeBaseShipColor({ legacy: legacy?.shipCode ?? null, sog: t.sog ?? null }),
selected: isSelected,
highlighted: isHighlighted,
permitted: legacy ? 1 : 0,
labelName: (t.name || '').trim() || legacy?.shipNameCn || legacy?.shipNameRoman || '',
legacyTag: legacy ? `${legacy.permitNo} (${legacy.shipCode})` : '',
};
}