import type { TrackPoint } from '../../../entities/vesselTrack/model/types'; import type { ProcessedTrack, TrackStats } from '../model/track.types'; const DEFAULT_SHIP_KIND = '000027'; const DEFAULT_SIGNAL_SOURCE = '000001'; const EPSILON_DISTANCE = 1e-10; function toFiniteNumber(value: unknown): number | null { if (typeof value === 'number') return Number.isFinite(value) ? value : null; if (typeof value === 'string') { const parsed = Number(value.trim()); return Number.isFinite(parsed) ? parsed : null; } return null; } export function normalizeTrackTimestampMs(value: string | number | undefined | null): number { if (typeof value === 'number') { return value < 1e12 ? value * 1000 : value; } if (typeof value === 'string' && value.trim().length > 0) { if (/^\d{10,}$/.test(value)) { const asNum = Number(value); return asNum < 1e12 ? asNum * 1000 : asNum; } const parsed = new Date(value).getTime(); if (Number.isFinite(parsed)) return parsed; } return Date.now(); } function calculateStats(points: TrackPoint[]): TrackStats { let maxSpeed = 0; let speedSum = 0; for (const point of points) { const speed = Number.isFinite(point.sog) ? point.sog : 0; maxSpeed = Math.max(maxSpeed, speed); speedSum += speed; } return { totalDistanceNm: 0, avgSpeed: points.length > 0 ? speedSum / points.length : 0, maxSpeed, pointCount: points.length, }; } export function convertLegacyTrackPointsToProcessedTrack( mmsi: number, points: TrackPoint[], hints?: { shipName?: string; shipKindCode?: string; nationalCode?: string; sigSrcCd?: string; }, ): ProcessedTrack | null { const sorted = [...points].sort( (a, b) => normalizeTrackTimestampMs(a.messageTimestamp) - normalizeTrackTimestampMs(b.messageTimestamp), ); if (sorted.length === 0) return null; const first = sorted[0]; const normalizedPoints = sorted .map((point) => { const lon = toFiniteNumber(point.lon); const lat = toFiniteNumber(point.lat); if (lon == null || lat == null) return null; const ts = normalizeTrackTimestampMs(point.messageTimestamp); const speed = toFiniteNumber(point.sog) ?? 0; return { point, lon, lat, ts, speed, }; }) .filter((entry): entry is NonNullable => entry != null); if (normalizedPoints.length === 0) return null; const geometry: [number, number][] = []; const timestampsMs: number[] = []; const speeds: number[] = []; const statsPoints: TrackPoint[] = []; for (const entry of normalizedPoints) { const lastCoord = geometry[geometry.length - 1]; const isDuplicateCoord = lastCoord != null && Math.abs(lastCoord[0] - entry.lon) <= EPSILON_DISTANCE && Math.abs(lastCoord[1] - entry.lat) <= EPSILON_DISTANCE; const lastTs = timestampsMs[timestampsMs.length - 1]; // Drop exact duplicate samples to avoid zero-length/duplicate segments. if (isDuplicateCoord && lastTs === entry.ts) continue; geometry.push([entry.lon, entry.lat]); timestampsMs.push(entry.ts); speeds.push(entry.speed); statsPoints.push(entry.point); } if (geometry.length === 0) return null; const stats = calculateStats(statsPoints); return { vesselId: `${hints?.sigSrcCd || DEFAULT_SIGNAL_SOURCE}_${mmsi}`, targetId: String(mmsi), sigSrcCd: hints?.sigSrcCd || DEFAULT_SIGNAL_SOURCE, shipName: (hints?.shipName || first.name || '').trim() || `MMSI ${mmsi}`, shipKindCode: hints?.shipKindCode || DEFAULT_SHIP_KIND, nationalCode: hints?.nationalCode || '', geometry, timestampsMs, speeds, stats, }; } export function getTracksTimeRange(tracks: ProcessedTrack[]): { start: number; end: number } | null { if (tracks.length === 0) return null; let min = Number.POSITIVE_INFINITY; let max = Number.NEGATIVE_INFINITY; for (const track of tracks) { if (track.timestampsMs.length === 0) continue; min = Math.min(min, track.timestampsMs[0]); max = Math.max(max, track.timestampsMs[track.timestampsMs.length - 1]); } if (!Number.isFinite(min) || !Number.isFinite(max) || min > max) return null; return { start: min, end: max }; } export function getShipKindColor(shipKindCode: string): [number, number, number, number] { const colors: Record = { '000020': [25, 116, 25, 180], '000021': [0, 41, 255, 180], '000022': [176, 42, 42, 180], '000023': [255, 139, 54, 180], '000024': [255, 0, 0, 180], '000025': [92, 30, 224, 180], '000027': [255, 135, 207, 180], '000028': [232, 95, 27, 180], }; return colors[shipKindCode] || colors['000027']; }