import { describe, it, expect } from "vitest"; import { generateGpx } from "./generate.ts"; import { parseGpxAsync } from "./parse.ts"; import { extractWaypoints } from "./waypoints.ts"; describe("no-go area generation", () => { it("includes trails namespace when no-go areas are present", () => { const gpx = generateGpx({ noGoAreas: [{ points: [{ lat: 52, lon: 13 }, { lat: 51, lon: 13 }, { lat: 51, lon: 14 }] }], }); expect(gpx).toContain('xmlns:trails="https://trails.cool/gpx/1"'); }); it("does not include trails namespace without no-go areas", () => { const gpx = generateGpx({ name: "test" }); expect(gpx).not.toContain("xmlns:trails"); expect(gpx).not.toContain(""); }); it("generates correct extension structure", () => { const gpx = generateGpx({ noGoAreas: [{ points: [{ lat: 52.5, lon: 13.3 }, { lat: 52.4, lon: 13.4 }, { lat: 52.3, lon: 13.2 }] }], }); expect(gpx).toContain(""); expect(gpx).toContain(""); expect(gpx).toContain(""); expect(gpx).toContain(''); expect(gpx).toContain(''); expect(gpx).toContain(''); }); it("generates multiple no-go areas", () => { const gpx = generateGpx({ noGoAreas: [ { points: [{ lat: 1, lon: 2 }, { lat: 3, lon: 4 }, { lat: 5, lon: 6 }] }, { points: [{ lat: 7, lon: 8 }, { lat: 9, lon: 10 }, { lat: 11, lon: 12 }] }, ], }); const matches = gpx.match(//g); expect(matches).toHaveLength(2); }); }); describe("no-go area parsing", () => { it("parses namespaced no-go areas", async () => { const gpx = ` `; const data = await parseGpxAsync(gpx); expect(data.noGoAreas).toHaveLength(1); expect(data.noGoAreas[0]!.points).toHaveLength(3); expect(data.noGoAreas[0]!.points[0]).toEqual({ lat: 52.5, lon: 13.3 }); }); it("parses non-namespaced no-go areas", async () => { const gpx = ` `; const data = await parseGpxAsync(gpx); expect(data.noGoAreas).toHaveLength(1); expect(data.noGoAreas[0]!.points).toHaveLength(3); }); it("rejects no-go areas with fewer than 3 points", async () => { const gpx = ` `; const data = await parseGpxAsync(gpx); expect(data.noGoAreas).toHaveLength(0); }); it("returns empty array when no extensions", async () => { const gpx = ` `; const data = await parseGpxAsync(gpx); expect(data.noGoAreas).toHaveLength(0); }); it("parses multiple no-go areas", async () => { const gpx = ` `; const data = await parseGpxAsync(gpx); expect(data.noGoAreas).toHaveLength(2); }); }); describe("no-go area round-trip", () => { it("round-trips no-go areas through generate and parse", async () => { const noGoAreas = [ { points: [{ lat: 52.5, lon: 13.3 }, { lat: 52.4, lon: 13.4 }, { lat: 52.3, lon: 13.2 }] }, { points: [{ lat: 48.1, lon: 11.5 }, { lat: 48.0, lon: 11.6 }, { lat: 47.9, lon: 11.4 }] }, ]; const gpx = generateGpx({ name: "No-go test", tracks: [[{ lat: 52.5, lon: 13.3, ele: 34 }, { lat: 48.1, lon: 11.5, ele: 519 }]], noGoAreas, }); const parsed = await parseGpxAsync(gpx); expect(parsed.name).toBe("No-go test"); expect(parsed.noGoAreas).toHaveLength(2); expect(parsed.noGoAreas[0]!.points).toEqual(noGoAreas[0]!.points); expect(parsed.noGoAreas[1]!.points).toEqual(noGoAreas[1]!.points); }); it("round-trips complete planning state", async () => { const gpx = generateGpx({ name: "Full plan", waypoints: [{ lat: 52.5, lon: 13.3, name: "Berlin" }, { lat: 48.1, lon: 11.5, name: "Munich" }], tracks: [[{ lat: 52.5, lon: 13.3, ele: 34 }, { lat: 50.0, lon: 12.0, ele: 300 }, { lat: 48.1, lon: 11.5, ele: 519 }]], noGoAreas: [{ points: [{ lat: 51, lon: 12 }, { lat: 50, lon: 13 }, { lat: 50, lon: 11 }] }], }); const parsed = await parseGpxAsync(gpx); expect(parsed.name).toBe("Full plan"); expect(parsed.waypoints).toHaveLength(2); expect(parsed.tracks[0]).toHaveLength(3); expect(parsed.noGoAreas).toHaveLength(1); expect(parsed.distance).toBeGreaterThan(0); }); }); describe("extractWaypoints", () => { it("returns explicit waypoints when present", async () => { const gpx = ` Berlin Munich `; const data = await parseGpxAsync(gpx); const wps = extractWaypoints(data); expect(wps).toHaveLength(2); expect(wps[0]!.name).toBe("Berlin"); }); it("extracts segment endpoints for multi-segment tracks", async () => { const gpx = ` `; const data = await parseGpxAsync(gpx); const wps = extractWaypoints(data); // Start of each segment (deduped) + end of last = 52, 51, 50, 48 expect(wps.length).toBeGreaterThanOrEqual(3); expect(wps[0]).toEqual({ lat: 52, lon: 13 }); }); it("uses Douglas-Peucker for single-segment tracks", async () => { // Build a track with a clear deviation in the middle const points = []; for (let i = 0; i <= 10; i++) { const lat = 52 - i * 0.4; // straight south const lon = i === 5 ? 14.5 : 13; // big eastward deviation at midpoint points.push(``); } const gpx = ` ${points.join("")} `; const data = await parseGpxAsync(gpx); const wps = extractWaypoints(data); // Should include start, end, and the midpoint deviation expect(wps.length).toBeGreaterThanOrEqual(3); // The deviated midpoint should be preserved const hasDeviation = wps.some((w) => Math.abs(w.lon - 14.5) < 0.01); expect(hasDeviation).toBe(true); }); it("returns empty for empty tracks", async () => { const gpx = ` `; const data = await parseGpxAsync(gpx); const wps = extractWaypoints(data); expect(wps).toHaveLength(0); }); });