Add multi-track GPX export option (splitByDay)
generateGpx() gains a splitByDay option that splits the track into separate <trk> elements per day, each named "Day N: Start - End". Track points are split at the closest points to overnight waypoints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dd8d72d7b3
commit
774e2b1ae3
2 changed files with 86 additions and 11 deletions
|
|
@ -13,6 +13,8 @@ export function generateGpx(options: {
|
|||
waypoints?: Waypoint[];
|
||||
tracks?: TrackPoint[][];
|
||||
noGoAreas?: NoGoArea[];
|
||||
/** When true, splits tracks at overnight waypoints into separate <trk> elements per day */
|
||||
splitByDay?: boolean;
|
||||
}): string {
|
||||
const hasExtensions = options.noGoAreas && options.noGoAreas.length > 0;
|
||||
const lines: string[] = [
|
||||
|
|
@ -41,19 +43,44 @@ export function generateGpx(options: {
|
|||
}
|
||||
|
||||
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>`);
|
||||
const allPoints = options.tracks.flat();
|
||||
|
||||
if (options.splitByDay && options.waypoints) {
|
||||
// Split track into per-day segments at overnight waypoints
|
||||
const dayTracks = splitTrackByDays(allPoints, options.waypoints);
|
||||
for (const dayTrack of dayTracks) {
|
||||
lines.push(" <trk>");
|
||||
if (dayTrack.name) {
|
||||
lines.push(` <name>${escapeXml(dayTrack.name)}</name>`);
|
||||
}
|
||||
if (pt.time) {
|
||||
lines.push(` <time>${escapeXml(pt.time)}</time>`);
|
||||
lines.push(" <trkseg>");
|
||||
for (const pt of dayTrack.points) {
|
||||
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(" </trkpt>");
|
||||
lines.push(" </trkseg>", " </trk>");
|
||||
}
|
||||
} else {
|
||||
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>");
|
||||
}
|
||||
lines.push(" </trkseg>", " </trk>");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +100,54 @@ export function generateGpx(options: {
|
|||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function splitTrackByDays(
|
||||
points: TrackPoint[],
|
||||
waypoints: Waypoint[],
|
||||
): Array<{ name: string; points: TrackPoint[] }> {
|
||||
if (points.length === 0 || waypoints.length === 0) return [];
|
||||
|
||||
// Find closest track point index for each waypoint
|
||||
const wpTrackIndices = waypoints.map((wp) => {
|
||||
let bestIdx = 0;
|
||||
let bestDist = Infinity;
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const dx = points[i]!.lat - wp.lat;
|
||||
const dy = points[i]!.lon - wp.lon;
|
||||
const d = dx * dx + dy * dy;
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
return bestIdx;
|
||||
});
|
||||
|
||||
// Build day boundaries from overnight waypoints
|
||||
const breakWpIndices = waypoints
|
||||
.map((wp, i) => (wp.isDayBreak ? i : -1))
|
||||
.filter((i) => i >= 0);
|
||||
|
||||
if (breakWpIndices.length === 0) {
|
||||
return [{ name: "Day 1", points }];
|
||||
}
|
||||
|
||||
const boundaries = [0, ...breakWpIndices.map((i) => wpTrackIndices[i]!), points.length];
|
||||
const dayTracks: Array<{ name: string; points: TrackPoint[] }> = [];
|
||||
|
||||
for (let d = 0; d < boundaries.length - 1; d++) {
|
||||
const start = boundaries[d]!;
|
||||
const end = boundaries[d + 1]!;
|
||||
const startWp = d === 0 ? waypoints[0] : waypoints[breakWpIndices[d - 1]!];
|
||||
const endWp = d < breakWpIndices.length ? waypoints[breakWpIndices[d]!] : waypoints[waypoints.length - 1];
|
||||
const startName = startWp?.name ?? "";
|
||||
const endName = endWp?.name ?? "";
|
||||
const name = startName && endName ? `Day ${d + 1}: ${startName} - ${endName}` : `Day ${d + 1}`;
|
||||
dayTracks.push({ name, points: points.slice(start, end) });
|
||||
}
|
||||
|
||||
return dayTracks;
|
||||
}
|
||||
|
||||
function escapeXml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue