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>
86 lines
2.2 KiB
TypeScript
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;
|
|
}
|