trails/apps/planner/app/lib/use-elevation-data.ts
Ullrich Schäfer c33a413395
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>
2026-07-15 17:07:28 +02:00

86 lines
2.2 KiB
TypeScript

import { useEffect, useState } from "react";
import * as Y from "yjs";
import type { ElevationPoint } from "~/lib/elevation-chart-draw";
import {
getColorMode,
readGeojson,
readRoadMetadata,
type ColorMode,
type RoadMetadata,
} from "~/lib/route-data";
function haversine(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371000;
const toRad = (d: number) => (d * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
function extractElevation(geojsonStr: string): ElevationPoint[] {
try {
const geojson = JSON.parse(geojsonStr);
const coords: number[][] = geojson.features?.[0]?.geometry?.coordinates ?? [];
if (coords.length === 0) return [];
const points: ElevationPoint[] = [];
let totalDist = 0;
for (let i = 0; i < coords.length; i++) {
if (i > 0) {
const prev = coords[i - 1]!;
const curr = coords[i]!;
totalDist += haversine(prev[1]!, prev[0]!, curr[1]!, curr[0]!);
}
if (coords[i]![2] !== undefined) {
points.push({
distance: totalDist,
elevation: coords[i]![2]!,
lat: coords[i]![1]!,
lon: coords[i]![0]!,
});
}
}
return points;
} catch {
return [];
}
}
export interface ElevationData extends RoadMetadata {
points: ElevationPoint[];
colorMode: ColorMode;
}
export function useElevationData(routeData: Y.Map<unknown>): ElevationData {
const [data, setData] = useState<ElevationData>({
points: [],
colorMode: "plain",
surfaces: [],
highways: [],
maxspeeds: [],
smoothnesses: [],
tracktypes: [],
cycleways: [],
bikeroutes: [],
});
useEffect(() => {
const update = () => {
const geojson = readGeojson(routeData);
setData({
points: geojson ? extractElevation(geojson) : [],
colorMode: getColorMode(routeData),
...readRoadMetadata(routeData),
});
};
routeData.observe(update);
update();
return () => routeData.unobserve(update);
}, [routeData]);
return data;
}