import { ROUTE_LEGS } from '../mocks/route'; import type { RouteLeg } from '../types/timeline'; const MILEAGE_ROUND_M = 10; const METERS_PER_KM = 1000; /** Mileage in meters at a cursor px, via piecewise-linear leg anchors. */ export function mileageAtPx(px: number, legs: RouteLeg[] = ROUTE_LEGS): number { const leg = legs.find((l) => px >= l.startPx && px <= l.endPx) ?? legs[legs.length - 1]; const anchors = leg.anchors; for (let i = 0; i < anchors.length - 1; i++) { const a = anchors[i]; const b = anchors[i + 1]; if (px <= b.px || i === anchors.length - 2) { const t = Math.min(1, Math.max(0, (px - a.px) / (b.px - a.px))); return a.mileage + (b.mileage - a.mileage) * t; } } return anchors[0].mileage; } /** * First px at which the given mileage occurs. Leg start anchors win so a jump * to a turn mileage lands on the start of that leg. */ export function pxForMileage( mileage: number, legs: RouteLeg[] = ROUTE_LEGS, ): number | null { for (const leg of legs) { if (leg.anchors[0].mileage === mileage) return leg.startPx; } for (const leg of legs) { const anchors = leg.anchors; for (let i = 0; i < anchors.length - 1; i++) { const a = anchors[i]; const b = anchors[i + 1]; const lo = Math.min(a.mileage, b.mileage); const hi = Math.max(a.mileage, b.mileage); if (mileage >= lo && mileage <= hi) { return a.px + (b.px - a.px) * ((mileage - a.mileage) / (b.mileage - a.mileage)); } } } return null; } /** 158204 → "158+200". */ export function formatMileage(mileage: number): string { const rounded = Math.round(mileage / MILEAGE_ROUND_M) * MILEAGE_ROUND_M; const km = Math.floor(rounded / METERS_PER_KM); const m = rounded % METERS_PER_KM; return `${km}+${String(m).padStart(3, '0')}`; } /** Accepts "158k200", "158200", "161800"… Returns meters or null. */ export function parseMileageQuery(raw: string): number | null { const digits = raw.toLowerCase().replace(/k/g, '').replace(/[^0-9]/g, ''); const mileage = parseInt(digits, 10); return Number.isFinite(mileage) && mileage > 0 ? mileage : null; }