- overnight.ts: Yjs helpers to set/clear/check overnight flag on waypoints - compute-days.ts: Pure function splitting routes into DayStage[] at day breaks - use-days.ts: Reactive hook mapping Yjs state into computeDays() input - GPX generate: emit <type>overnight</type> for isDayBreak waypoints - GPX parse: recognize <type>overnight</type> and set isDayBreak on waypoints Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
83 lines
2.4 KiB
TypeScript
83 lines
2.4 KiB
TypeScript
import type { Waypoint } from "@trails-cool/types";
|
|
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"' +
|
|
(hasExtensions ? '\n xmlns:trails="https://trails.cool/gpx/1"' : "") +
|
|
">",
|
|
];
|
|
|
|
if (options.name) {
|
|
lines.push(" <metadata>", ` <name>${escapeXml(options.name)}</name>`, " </metadata>");
|
|
}
|
|
|
|
if (options.waypoints) {
|
|
for (const wpt of options.waypoints) {
|
|
lines.push(` <wpt lat="${wpt.lat}" lon="${wpt.lon}">`);
|
|
if (wpt.name) {
|
|
lines.push(` <name>${escapeXml(wpt.name)}</name>`);
|
|
}
|
|
if (wpt.isDayBreak) {
|
|
lines.push(" <type>overnight</type>");
|
|
}
|
|
lines.push(" </wpt>");
|
|
}
|
|
}
|
|
|
|
if (options.tracks) {
|
|
for (const track of options.tracks) {
|
|
lines.push(" <trk>", " <trkseg>");
|
|
for (const pt of track) {
|
|
lines.push(` <trkpt lat="${pt.lat}" lon="${pt.lon}">`);
|
|
if (pt.ele !== undefined) {
|
|
lines.push(` <ele>${pt.ele}</ele>`);
|
|
}
|
|
if (pt.time) {
|
|
lines.push(` <time>${escapeXml(pt.time)}</time>`);
|
|
}
|
|
lines.push(" </trkpt>");
|
|
}
|
|
lines.push(" </trkseg>", " </trk>");
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
function escapeXml(str: string): string {
|
|
return str
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|