Add split export: Export Route vs Export Plan
Split button with default "Export GPX" (track only) and dropdown: - Export Route: clean track for use in any app - Export Plan: track + waypoints + no-go areas in GPX extensions (trails:planning namespace) for reimporting into the planner Also: - Add no-go area serialization to generateGpx (GPX extensions) - Parse no-go areas from GPX extensions on import - Initialize no-go areas in Yjs session from imported GPX - Save to Journal now exports track only (consistent with Export Route) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
237660db5e
commit
4b711abaec
11 changed files with 172 additions and 60 deletions
|
|
@ -4,15 +4,23 @@ import type { TrackPoint } from "./types.ts";
|
|||
/**
|
||||
* Generate a GPX XML string from waypoints and track points.
|
||||
*/
|
||||
export interface NoGoArea {
|
||||
points: Array<{ lat: number; lon: number }>;
|
||||
}
|
||||
|
||||
export function generateGpx(options: {
|
||||
name?: string;
|
||||
waypoints?: Waypoint[];
|
||||
tracks?: TrackPoint[][];
|
||||
noGoAreas?: NoGoArea[];
|
||||
}): string {
|
||||
const hasExtensions = options.noGoAreas && options.noGoAreas.length > 0;
|
||||
const lines: string[] = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<gpx version="1.1" creator="trails.cool"',
|
||||
' xmlns="http://www.topografix.com/GPX/1/1">',
|
||||
' xmlns="http://www.topografix.com/GPX/1/1"' +
|
||||
(hasExtensions ? '\n xmlns:trails="https://trails.cool/gpx/1"' : "") +
|
||||
">",
|
||||
];
|
||||
|
||||
if (options.name) {
|
||||
|
|
@ -46,6 +54,18 @@ export function generateGpx(options: {
|
|||
}
|
||||
}
|
||||
|
||||
if (options.noGoAreas && options.noGoAreas.length > 0) {
|
||||
lines.push(" <extensions>", " <trails:planning>");
|
||||
for (const area of options.noGoAreas) {
|
||||
lines.push(" <trails:nogo>");
|
||||
for (const pt of area.points) {
|
||||
lines.push(` <trails:point lat="${pt.lat}" lon="${pt.lon}"/>`);
|
||||
}
|
||||
lines.push(" </trails:nogo>");
|
||||
}
|
||||
lines.push(" </trails:planning>", " </extensions>");
|
||||
}
|
||||
|
||||
lines.push("</gpx>");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export { parseGpxAsync } from "./parse.ts";
|
||||
export { generateGpx } from "./generate.ts";
|
||||
export { extractWaypoints } from "./waypoints.ts";
|
||||
export type { GpxData, TrackPoint, ElevationProfile } from "./types.ts";
|
||||
export type { GpxData, TrackPoint, ElevationProfile, NoGoArea } from "./types.ts";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Waypoint } from "@trails-cool/types";
|
||||
import type { GpxData, TrackPoint, ElevationProfile } from "./types.ts";
|
||||
import type { GpxData, TrackPoint, ElevationProfile, NoGoArea } from "./types.ts";
|
||||
|
||||
/**
|
||||
* Parse a GPX XML string into structured data.
|
||||
|
|
@ -46,9 +46,10 @@ function parseGpxWithParser(parser: DOMParser, xml: string): GpxData {
|
|||
const name = doc.querySelector("metadata > name")?.textContent ?? undefined;
|
||||
const waypoints = parseWaypoints(doc);
|
||||
const tracks = parseTracks(doc);
|
||||
const noGoAreas = parseNoGoAreas(doc);
|
||||
const { totalDistance, ...elevation } = computeElevation(tracks);
|
||||
|
||||
return { name, waypoints, tracks, distance: totalDistance, elevation };
|
||||
return { name, waypoints, tracks, noGoAreas, distance: totalDistance, elevation };
|
||||
}
|
||||
|
||||
function parseWaypoints(doc: Document): Waypoint[] {
|
||||
|
|
@ -85,6 +86,22 @@ function parseTracks(doc: Document): TrackPoint[][] {
|
|||
return tracks;
|
||||
}
|
||||
|
||||
function parseNoGoAreas(doc: Document): NoGoArea[] {
|
||||
const areas: NoGoArea[] = [];
|
||||
const nogos = doc.querySelectorAll("nogo");
|
||||
for (const nogo of Array.from(nogos)) {
|
||||
const points: Array<{ lat: number; lon: number }> = [];
|
||||
const pts = nogo.querySelectorAll("point");
|
||||
for (const pt of Array.from(pts)) {
|
||||
const lat = parseFloat(pt.getAttribute("lat") ?? "0");
|
||||
const lon = parseFloat(pt.getAttribute("lon") ?? "0");
|
||||
points.push({ lat, lon });
|
||||
}
|
||||
if (points.length >= 3) areas.push({ points });
|
||||
}
|
||||
return areas;
|
||||
}
|
||||
|
||||
function computeElevation(tracks: TrackPoint[][]): GpxData["elevation"] & { totalDistance: number } {
|
||||
let gain = 0;
|
||||
let loss = 0;
|
||||
|
|
|
|||
|
|
@ -14,10 +14,15 @@ export interface ElevationProfile {
|
|||
elevation: number;
|
||||
}
|
||||
|
||||
export interface NoGoArea {
|
||||
points: Array<{ lat: number; lon: number }>;
|
||||
}
|
||||
|
||||
export interface GpxData {
|
||||
name?: string;
|
||||
waypoints: Waypoint[];
|
||||
tracks: TrackPoint[][];
|
||||
noGoAreas: NoGoArea[];
|
||||
/** Total distance in meters (haversine, works with or without elevation data) */
|
||||
distance: number;
|
||||
elevation: {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ export default {
|
|||
newSession: "Neue Sitzung",
|
||||
saveRoute: "Route speichern",
|
||||
exportGpx: "GPX exportieren",
|
||||
exportRoute: "Route exportieren",
|
||||
exportPlan: "Plan exportieren",
|
||||
profile: "Profil",
|
||||
connecting: "Verbinde...",
|
||||
loadingMap: "Karte wird geladen...",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ export default {
|
|||
newSession: "New Session",
|
||||
saveRoute: "Save Route",
|
||||
exportGpx: "Export GPX",
|
||||
exportRoute: "Export Route",
|
||||
exportPlan: "Export Plan",
|
||||
profile: "Profile",
|
||||
connecting: "Connecting...",
|
||||
loadingMap: "Loading map...",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue