import type { TrackSegment, InterpolatedPosition } from '../types' /** * 이진 탐색으로 currentTime이 속하는 구간 [lo, hi] 인덱스 반환 * O(log n) — 프레임당 선박 수 × O(log n) */ function findTimeIndex(timestampsMs: number[], currentTime: number): [number, number] { let lo = 0 let hi = timestampsMs.length - 1 while (lo < hi - 1) { const mid = (lo + hi) >> 1 if (timestampsMs[mid] <= currentTime) lo = mid else hi = mid } return [lo, hi] } /** 두 점 사이 방위각 계산 (degrees, 0-360) */ function calculateHeading( lon1: number, lat1: number, lon2: number, lat2: number, ): number { const dLon = ((lon2 - lon1) * Math.PI) / 180 const lat1Rad = (lat1 * Math.PI) / 180 const lat2Rad = (lat2 * Math.PI) / 180 const y = Math.sin(dLon) * Math.cos(lat2Rad) const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLon) let heading = (Math.atan2(y, x) * 180) / Math.PI if (heading < 0) heading += 360 return heading } /** * 트랙 배열에서 currentTime 기준 보간된 위치 목록 생성 * disabledIds에 포함된 vesselId는 제외 */ export function getPositionsAtTime( tracks: TrackSegment[], currentTime: number, disabledIds?: Set, ): InterpolatedPosition[] { const positions: InterpolatedPosition[] = [] for (const track of tracks) { if (disabledIds?.has(track.vesselId)) continue const { timestampsMs, geometry, speeds } = track if (timestampsMs.length === 0) continue // 경계값 처리 if (currentTime <= timestampsMs[0]) { positions.push({ vesselId: track.vesselId, lon: geometry[0][0], lat: geometry[0][1], heading: 0, speed: speeds[0] || 0, shipName: track.shipName || '', shipKindCode: track.shipKindCode || '', }) continue } if (currentTime >= timestampsMs[timestampsMs.length - 1]) { const last = geometry.length - 1 positions.push({ vesselId: track.vesselId, lon: geometry[last][0], lat: geometry[last][1], heading: 0, speed: speeds[last] || 0, shipName: track.shipName || '', shipKindCode: track.shipKindCode || '', }) continue } // 이진 탐색 + 선형 보간 const [lo, hi] = findTimeIndex(timestampsMs, currentTime) const t1 = timestampsMs[lo] const t2 = timestampsMs[hi] const ratio = t2 === t1 ? 0 : (currentTime - t1) / (t2 - t1) const p1 = geometry[lo] const p2 = geometry[hi] const lon = p1[0] + (p2[0] - p1[0]) * ratio const lat = p1[1] + (p2[1] - p1[1]) * ratio const heading = calculateHeading(p1[0], p1[1], p2[0], p2[1]) const speed = speeds[lo] + (speeds[hi] - speeds[lo]) * ratio positions.push({ vesselId: track.vesselId, lon, lat, heading, speed, shipName: track.shipName || '', shipKindCode: track.shipKindCode || '', }) } return positions } /** 트랙 배열의 전체 시간 범위 [min, max] (ms) */ export function getTimeRange(tracks: TrackSegment[]): [number, number] { let min = Infinity let max = -Infinity for (const t of tracks) { if (t.timestampsMs.length === 0) continue const first = t.timestampsMs[0] const last = t.timestampsMs[t.timestampsMs.length - 1] if (first < min) min = first if (last > max) max = last } return [min, max] }