Implement shared packages (tasks 2.1-2.6)
- @trails-cool/types: Route, Activity, Waypoint, RouteVersion, RouteMetadata - @trails-cool/gpx: GPX parser (XML→waypoints/tracks/elevation) and generator with round-trip test, haversine distance, elevation gain/loss computation - @trails-cool/map: MapView (Leaflet + OSM/OpenTopoMap/CyclOSM layer switcher), RouteLayer (GeoJSON polyline rendering) - @trails-cool/ui: Button (primary/secondary/ghost), Input (with label/error), Card components with Tailwind styling - @trails-cool/i18n: react-i18next config with English + German translations, browser language detection, fallback to English All 13 unit tests pass. Both apps build successfully. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
325a4466d5
commit
2cfa5e54e7
23 changed files with 809 additions and 30 deletions
56
packages/gpx/src/parse.test.ts
Normal file
56
packages/gpx/src/parse.test.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { parseGpx } from "./parse";
|
||||
|
||||
const sampleGpx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx version="1.1" creator="test" xmlns="http://www.topografix.com/GPX/1/1">
|
||||
<metadata><name>Test Route</name></metadata>
|
||||
<wpt lat="52.52" lon="13.405"><name>Berlin</name></wpt>
|
||||
<wpt lat="48.137" lon="11.576"><name>Munich</name></wpt>
|
||||
<trk>
|
||||
<trkseg>
|
||||
<trkpt lat="52.52" lon="13.405"><ele>34</ele></trkpt>
|
||||
<trkpt lat="51.05" lon="13.74"><ele>113</ele></trkpt>
|
||||
<trkpt lat="48.137" lon="11.576"><ele>519</ele></trkpt>
|
||||
</trkseg>
|
||||
</trk>
|
||||
</gpx>`;
|
||||
|
||||
describe("parseGpx", () => {
|
||||
it("parses route name", () => {
|
||||
const result = parseGpx(sampleGpx);
|
||||
expect(result.name).toBe("Test Route");
|
||||
});
|
||||
|
||||
it("parses waypoints with lat, lon, and name", () => {
|
||||
const result = parseGpx(sampleGpx);
|
||||
expect(result.waypoints).toHaveLength(2);
|
||||
expect(result.waypoints[0]).toEqual({ lat: 52.52, lon: 13.405, name: "Berlin" });
|
||||
expect(result.waypoints[1]).toEqual({ lat: 48.137, lon: 11.576, name: "Munich" });
|
||||
});
|
||||
|
||||
it("parses track points with elevation", () => {
|
||||
const result = parseGpx(sampleGpx);
|
||||
expect(result.tracks).toHaveLength(1);
|
||||
expect(result.tracks[0]).toHaveLength(3);
|
||||
expect(result.tracks[0]![0]).toEqual({ lat: 52.52, lon: 13.405, ele: 34, time: undefined });
|
||||
});
|
||||
|
||||
it("computes elevation gain and loss", () => {
|
||||
const result = parseGpx(sampleGpx);
|
||||
expect(result.elevation.gain).toBeGreaterThan(0);
|
||||
expect(result.elevation.loss).toBe(0); // monotonically increasing elevation
|
||||
expect(result.elevation.gain).toBe(485); // 113-34 + 519-113
|
||||
});
|
||||
|
||||
it("builds elevation profile", () => {
|
||||
const result = parseGpx(sampleGpx);
|
||||
expect(result.elevation.profile).toHaveLength(3);
|
||||
expect(result.elevation.profile[0]!.distance).toBe(0);
|
||||
expect(result.elevation.profile[0]!.elevation).toBe(34);
|
||||
expect(result.elevation.profile[2]!.distance).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("throws on invalid XML", () => {
|
||||
expect(() => parseGpx("not xml at all <<<<")).toThrow("Invalid GPX XML");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue