Split ElevationChart + PlannerMap into deep modules; add visual regression testing
ElevationChart (1013 lines) split into: - elevation-chart-draw.ts — pure drawElevationChart(ctx, w, h, params) function - use-elevation-data.ts — useElevationData(routeData) hook - ElevationChart.tsx — ~300 lines, interaction + render only PlannerMap (869 lines) split into: - MapHelpers.tsx — 7 Leaflet sub-components (MapExposer, RouteFitter, MapClickHandler, CursorTracker, NoGoAreaButton, OverlaySync, PoiRefresher) - use-waypoint-manager.ts — all waypoint CRUD + route data sync - use-gpx-drop.ts — GPX drag-and-drop hook - PlannerMap.tsx — ~200 lines, orchestration only Add Vitest browser visual regression tests for drawElevationChart via @vitest/browser + Playwright (toMatchScreenshot). Tests cover plain, grade, elevation, surface color modes plus hover and drag-select states. Add update-visual-snapshots.yml workflow: triggered by workflow_dispatch or the `update-snapshots` PR label; commits snapshots back to the branch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6c5fb6df0e
commit
34529a9432
13 changed files with 1764 additions and 1322 deletions
98
apps/planner/app/lib/use-elevation-data.ts
Normal file
98
apps/planner/app/lib/use-elevation-data.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import * as Y from "yjs";
|
||||
import type { ColorMode } from "~/components/ColoredRoute";
|
||||
import type { ElevationPoint } from "~/lib/elevation-chart-draw";
|
||||
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonArray(json: string | undefined): string[] {
|
||||
if (!json) return [];
|
||||
try { return JSON.parse(json); } catch { return []; }
|
||||
}
|
||||
|
||||
export interface ElevationData {
|
||||
points: ElevationPoint[];
|
||||
colorMode: ColorMode;
|
||||
surfaces: string[];
|
||||
highways: string[];
|
||||
maxspeeds: string[];
|
||||
smoothnesses: string[];
|
||||
tracktypes: string[];
|
||||
cycleways: string[];
|
||||
bikeroutes: string[];
|
||||
}
|
||||
|
||||
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 = routeData.get("geojson") as string | undefined;
|
||||
setData({
|
||||
points: geojson ? extractElevation(geojson) : [],
|
||||
colorMode: (routeData.get("colorMode") as ColorMode | undefined) ?? "plain",
|
||||
surfaces: parseJsonArray(routeData.get("surfaces") as string | undefined),
|
||||
highways: parseJsonArray(routeData.get("highways") as string | undefined),
|
||||
maxspeeds: parseJsonArray(routeData.get("maxspeeds") as string | undefined),
|
||||
smoothnesses: parseJsonArray(routeData.get("smoothnesses") as string | undefined),
|
||||
tracktypes: parseJsonArray(routeData.get("tracktypes") as string | undefined),
|
||||
cycleways: parseJsonArray(routeData.get("cycleways") as string | undefined),
|
||||
bikeroutes: parseJsonArray(routeData.get("bikeroutes") as string | undefined),
|
||||
});
|
||||
};
|
||||
routeData.observe(update);
|
||||
update();
|
||||
return () => routeData.unobserve(update);
|
||||
}, [routeData]);
|
||||
|
||||
return data;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue