feat(planner): store computed route compact-encoded (groups 2–4)
Wire the map-core codec into route-data.ts so the Yjs session doc stores the computed route compactly, without changing the routing-host compute-once-and-share model. - Codec: add encodeElevations/decodeElevations (delta+varint) so geometry = encoded [lon,lat] polyline + elevation channel; precision bumped to 1e6 (~0.11 m) to preserve the router's 6-decimal output. - writeComputedRoute: geometry stored once (encoded polyline + elevations, no redundant geojson), road metadata run-length encoded. - Dual-format reads: getCoordinates / readRoadMetadata / new readGeojson transparently handle the new encoding AND legacy JSON docs (+ oldest geojson-only) — no migration, no flag day; legacy docs re-encode on the next recompute. use-elevation-data reads readGeojson (assembled for new docs). - Spec correction: a lossy compact codec can't be byte-identical to legacy (generateGpx emits raw precision), so "GPX byte-compatible" is corrected to "coordinates preserved within the router's ~0.1 m precision". Tests: legacy-format read, size assertion (>4x smaller / round-trips), elevation round-trip. map-core + planner typecheck/lint clean; full pnpm test 11/11. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
84744567bc
commit
c33a413395
8 changed files with 181 additions and 33 deletions
|
|
@ -33,5 +33,6 @@ export type { SurfaceBreakdown } from "./surface-breakdown.ts";
|
|||
|
||||
export {
|
||||
encodePolyline, decodePolyline, encodeRuns, decodeRuns,
|
||||
isEncodedPolyline, isEncodedRuns,
|
||||
encodeElevations, decodeElevations,
|
||||
isEncodedPolyline, isEncodedRuns, isEncodedElevations,
|
||||
} from "./route-codec.ts";
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ import {
|
|||
decodePolyline,
|
||||
encodeRuns,
|
||||
decodeRuns,
|
||||
encodeElevations,
|
||||
decodeElevations,
|
||||
isEncodedPolyline,
|
||||
isEncodedRuns,
|
||||
isEncodedElevations,
|
||||
} from "./route-codec.ts";
|
||||
|
||||
describe("polyline codec", () => {
|
||||
|
|
@ -55,6 +58,26 @@ describe("polyline codec", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("elevation codec", () => {
|
||||
it("round-trips elevations within decimetre precision", () => {
|
||||
const eles = [34, 34.2, 80.5, 519, 518.9, 200, 0, -5.3];
|
||||
const decoded = decodeElevations(encodeElevations(eles));
|
||||
expect(decoded).toHaveLength(eles.length);
|
||||
for (let i = 0; i < eles.length; i++) {
|
||||
expect(Math.abs(decoded[i]! - eles[i]!)).toBeLessThanOrEqual(0.05);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles empty and is much smaller than JSON for a long climb", () => {
|
||||
expect(decodeElevations(encodeElevations([]))).toEqual([]);
|
||||
const eles = Array.from({ length: 4000 }, (_, i) => 100 + i * 0.1);
|
||||
const enc = encodeElevations(eles);
|
||||
expect(isEncodedElevations(enc)).toBe(true);
|
||||
expect(enc.length).toBeLessThan(JSON.stringify(eles).length / 2);
|
||||
expect(decodeElevations(enc)).toHaveLength(4000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("run-length codec", () => {
|
||||
it("round-trips and collapses uniform runs", () => {
|
||||
const values = [
|
||||
|
|
|
|||
|
|
@ -5,9 +5,15 @@
|
|||
// path can tell them apart from the legacy JSON encoding (which is a bare
|
||||
// `[` … `]`), enabling transparent backward compatibility.
|
||||
|
||||
const PRECISION = 100_000; // 1e5 ≈ 1.1 m; below BRouter/OSM working resolution
|
||||
// 1e6 ≈ 0.11 m: matches the 6-decimal precision BRouter/GPX emit, so a
|
||||
// round-trip preserves the router's own coordinates (GPX export is
|
||||
// unchanged in practice). Values stay well within 32-bit for real
|
||||
// lon/lat ranges (±180e6 → ~29 bits after zig-zag).
|
||||
const PRECISION = 1_000_000;
|
||||
const ELEV_PRECISION = 10; // decimetre — finer than any real elevation source
|
||||
const POLYLINE_PREFIX = "p1:";
|
||||
const RUNS_PREFIX = "r1:";
|
||||
const ELEV_PREFIX = "e1:";
|
||||
|
||||
// --- base64 via the global btoa/atob (present in browsers and Node 18+),
|
||||
// so this stays framework-free with no @types/node dependency ---
|
||||
|
|
@ -58,6 +64,38 @@ export function isEncodedPolyline(s: string): boolean {
|
|||
export function isEncodedRuns(s: string): boolean {
|
||||
return s.startsWith(RUNS_PREFIX);
|
||||
}
|
||||
export function isEncodedElevations(s: string): boolean {
|
||||
return s.startsWith(ELEV_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a per-coordinate elevation channel (metres) as delta + zig-zag
|
||||
* varint over decimetre-precision integers. Elevation is the third
|
||||
* coordinate dimension; the horizontal [lon,lat] pairs go through
|
||||
* {@link encodePolyline}, so the two together carry the full [lon,lat,ele].
|
||||
*/
|
||||
export function encodeElevations(eles: readonly number[]): string {
|
||||
const bytes: number[] = [];
|
||||
let prev = 0;
|
||||
for (const e of eles) {
|
||||
const iv = Math.round(e * ELEV_PRECISION);
|
||||
writeVarint(bytes, zigzag(iv - prev));
|
||||
prev = iv;
|
||||
}
|
||||
return ELEV_PREFIX + bytesToBase64(Uint8Array.from(bytes));
|
||||
}
|
||||
|
||||
export function decodeElevations(s: string): number[] {
|
||||
if (!isEncodedElevations(s)) throw new Error("not encoded elevations");
|
||||
const nums = readVarints(base64ToBytes(s.slice(ELEV_PREFIX.length)));
|
||||
const out: number[] = [];
|
||||
let e = 0;
|
||||
for (const n of nums) {
|
||||
e += unzigzag(n);
|
||||
out.push(e / ELEV_PRECISION);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode `[lon, lat]` coordinates as delta + zig-zag varint over
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue