- geo.ts: haversineM + deriveCumulativeDistanceM (D-07 snapped-progress field) - fixture-source.ts: backend #1 — ORIGIN_TOLERANCE_M=200 matching law, honest-null pending semantics (D-03), defensive pair skip at index build, lazy memoized index (LAW 1), static JSON import (LAW 2), zero logging/storage/network (LAW 3, D-12) - index.ts: named-export public surface, subpath-with-extension import law - loader proven in three environments (bare Node 24, esbuild, Vite-family)
50 lines
2.1 KiB
TypeScript
50 lines
2.1 KiB
TypeScript
/*
|
|
* geo.ts — spherical geo math for the route model (D-07).
|
|
* --------------------------------------------------------
|
|
* haversineM: great-circle distance in meters on the mean-earth sphere
|
|
* (radius 6 371 000 m — at KL-metro scale the model error is far below the
|
|
* fixture corpus's 200 m origin tolerance). Finite for finite inputs.
|
|
*
|
|
* deriveCumulativeDistanceM: the snapped-progress support field (D-07 — the
|
|
* GUID-05/06 upstream obligation). Produces [0, d(p0→p1), d(p0→p1)+d(p1→p2),
|
|
* …] — the same length as the input, [0] === 0, non-decreasing. Phase 6's
|
|
* guidance snaps a live fix's progress along the line using this array
|
|
* instead of re-deriving it per fix; the fixture source derives it once at
|
|
* decode/load and memoizes (RESEARCH Finding 1).
|
|
*
|
|
* Pure: no I/O, no logging, no storage (the D-15 analog — nothing about a
|
|
* request outlives the caller's frame).
|
|
*/
|
|
|
|
import type { RoutePoint } from "./types.ts";
|
|
|
|
const EARTH_RADIUS_M = 6_371_000;
|
|
const DEGREES_TO_RADIANS = Math.PI / 180;
|
|
|
|
/** Great-circle distance between two WGS84 points, in meters. */
|
|
export function haversineM(a: RoutePoint, b: RoutePoint): number {
|
|
const lat1 = a.lat * DEGREES_TO_RADIANS;
|
|
const lat2 = b.lat * DEGREES_TO_RADIANS;
|
|
const dLat = (b.lat - a.lat) * DEGREES_TO_RADIANS;
|
|
const dLng = (b.lng - a.lng) * DEGREES_TO_RADIANS;
|
|
const sinLat = Math.sin(dLat / 2);
|
|
const sinLng = Math.sin(dLng / 2);
|
|
const h = sinLat * sinLat + Math.cos(lat1) * Math.cos(lat2) * sinLng * sinLng;
|
|
return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(h)));
|
|
}
|
|
|
|
/** Meters walked along the polyline: same length as points, [0] === 0,
|
|
* non-decreasing. Empty -> []; single point -> [0]. */
|
|
export function deriveCumulativeDistanceM(points: readonly RoutePoint[]): number[] {
|
|
if (points.length === 0) return [];
|
|
const cumulative: number[] = [0];
|
|
for (let i = 1; i < points.length; i++) {
|
|
const prev = points[i - 1];
|
|
const curr = points[i];
|
|
const prevMeters = cumulative[i - 1];
|
|
if (prev === undefined || curr === undefined || prevMeters === undefined) break;
|
|
cumulative.push(prevMeters + haversineM(prev, curr));
|
|
}
|
|
return cumulative;
|
|
}
|