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] { 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, 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})` : '', }; }