trails/packages/gpx/src/elevation-series.test.ts
Ullrich Schäfer 536a8f98b9
journal-elevation-profile: elevation chart + map↔chart sync
Implements the journal-elevation-profile change (specs/journal-elevation-profile):

- gpx: `elevationSeries(tracks)` → { d, e, lat, lng }[] with cumulative distance,
  downsampled (keeps first/last), empty when <2 points carry elevation.
- ElevationProfile: read-only SVG area chart (vertical gradient fill), highest/
  lowest summary + a hover readout. Reports hovered index (onActive) and clicked
  index (onSeek); draws a marker at the active index. Renders nothing for an
  empty series.
- RouteMapThumbnail: ActiveMarker (CircleMarker at the chart's active point),
  HoverTracker (route hover → nearest sample → onHoverIndex), Recenter (panTo on
  chart click). Props forwarded through ClientMap.
- Wired into the activity + route detail pages via a shared activeIndex/centerOn
  state; loaders expose the series (activity reuses the moving-time parse).
- i18n journal.elevation.{highest,lowest} in en + de.

Ascent/descent stay in the stat row (#532); the chart summary shows highest/
lowest to avoid duplication.

Tests: elevationSeries unit; ElevationProfile component (jsdom); e2e creates an
activity from an elevation GPX and asserts the chart renders. typecheck + lint +
unit (gpx 67, journal 315) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 15:34:45 +02:00

43 lines
1.5 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { elevationSeries } from "./elevation-series.ts";
import type { TrackPoint } from "./types.ts";
function pt(lat: number, lon: number, ele?: number): TrackPoint {
return { lat, lon, ele };
}
describe("elevationSeries", () => {
it("returns empty when fewer than two points carry elevation", () => {
expect(elevationSeries([[pt(0, 0), pt(0, 0.001)]])).toEqual([]);
expect(elevationSeries([[pt(0, 0, 100)]])).toEqual([]);
});
it("builds cumulative distance, elevation and position", () => {
const s = elevationSeries([[pt(0, 0, 100), pt(0, 0.001, 110), pt(0, 0.002, 105)]]);
expect(s).toHaveLength(3);
expect(s[0]!.d).toBe(0);
expect(s[0]!.e).toBe(100);
expect(s[1]!.d).toBeGreaterThan(0);
expect(s[2]!.d).toBeGreaterThan(s[1]!.d);
expect(s[2]!.e).toBe(105);
expect(s[0]!.lat).toBe(0);
});
it("flattens multiple track segments", () => {
const s = elevationSeries([
[pt(0, 0, 100), pt(0, 0.001, 110)],
[pt(0, 0.002, 120), pt(0, 0.003, 130)],
]);
expect(s).toHaveLength(4);
expect(s.map((x) => x.e)).toEqual([100, 110, 120, 130]);
});
it("downsamples to at most maxPoints, keeping first and last", () => {
const seg: TrackPoint[] = [];
for (let i = 0; i < 1000; i++) seg.push(pt(0, i * 0.0001, 100 + i));
const s = elevationSeries([seg], 100);
expect(s.length).toBeLessThanOrEqual(101);
expect(s[0]!.e).toBe(100);
expect(s[s.length - 1]!.e).toBe(1099);
});
});