import { haversineNm } from '../../../shared/lib/geo/haversineNm'; import type { ActiveTrack, NormalizedTrip } from '../model/types'; /** 시간순 정렬 후 TripsLayer용 정규화 데이터 생성 */ export function normalizeTrip( track: ActiveTrack, color: [number, number, number], ): NormalizedTrip { const sorted = [...track.points].sort( (a, b) => new Date(a.messageTimestamp).getTime() - new Date(b.messageTimestamp).getTime(), ); if (sorted.length === 0) { return { path: [], timestamps: [], mmsi: track.mmsi, name: '', color }; } const baseEpoch = new Date(sorted[0].messageTimestamp).getTime(); const path: [number, number][] = []; const timestamps: number[] = []; for (const pt of sorted) { path.push([pt.lon, pt.lat]); // 32-bit float 정밀도를 보장하기 위해 첫 포인트 기준 초 단위 오프셋 timestamps.push((new Date(pt.messageTimestamp).getTime() - baseEpoch) / 1000); } return { path, timestamps, mmsi: track.mmsi, name: sorted[0].name || `MMSI ${track.mmsi}`, color, }; } /** Globe 전용 — LineString GeoJSON */ export function buildTrackLineGeoJson( track: ActiveTrack, ): GeoJSON.FeatureCollection { const sorted = [...track.points].sort( (a, b) => new Date(a.messageTimestamp).getTime() - new Date(b.messageTimestamp).getTime(), ); if (sorted.length < 2) { return { type: 'FeatureCollection', features: [] }; } let totalDistanceNm = 0; const coordinates: [number, number][] = []; for (let i = 0; i < sorted.length; i++) { const pt = sorted[i]; coordinates.push([pt.lon, pt.lat]); if (i > 0) { const prev = sorted[i - 1]; totalDistanceNm += haversineNm(prev.lat, prev.lon, pt.lat, pt.lon); } } return { type: 'FeatureCollection', features: [ { type: 'Feature', properties: { mmsi: track.mmsi, name: sorted[0].name || `MMSI ${track.mmsi}`, pointCount: sorted.length, minutes: track.minutes, totalDistanceNm: Math.round(totalDistanceNm * 100) / 100, }, geometry: { type: 'LineString', coordinates }, }, ], }; } /** Globe+Mercator 공용 — Point GeoJSON */ export function buildTrackPointsGeoJson( track: ActiveTrack, ): GeoJSON.FeatureCollection { const sorted = [...track.points].sort( (a, b) => new Date(a.messageTimestamp).getTime() - new Date(b.messageTimestamp).getTime(), ); return { type: 'FeatureCollection', features: sorted.map((pt, index) => ({ type: 'Feature' as const, properties: { mmsi: pt.mmsi, name: pt.name, sog: pt.sog, cog: pt.cog, heading: pt.heading, status: pt.status, messageTimestamp: pt.messageTimestamp, index, }, geometry: { type: 'Point' as const, coordinates: [pt.lon, pt.lat] }, })), }; } export function getTrackTimeRange(trip: NormalizedTrip): { minTime: number; maxTime: number; durationSec: number; } { if (trip.timestamps.length === 0) { return { minTime: 0, maxTime: 0, durationSec: 0 }; } const minTime = trip.timestamps[0]; const maxTime = trip.timestamps[trip.timestamps.length - 1]; return { minTime, maxTime, durationSec: maxTime - minTime }; }