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
|
|
@ -1,60 +1,110 @@
|
|||
import { useCallback } from "react";
|
||||
import { useCallback, useState, useRef, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import * as Y from "yjs";
|
||||
import type { YjsState } from "~/lib/use-yjs";
|
||||
import { generateGpx } from "@trails-cool/gpx";
|
||||
import type { TrackPoint } from "@trails-cool/gpx";
|
||||
import type { TrackPoint, NoGoArea } from "@trails-cool/gpx";
|
||||
|
||||
function getTracks(yjs: YjsState): TrackPoint[][] {
|
||||
const geojsonStr = yjs.routeData.get("geojson") as string | undefined;
|
||||
if (!geojsonStr) return [];
|
||||
try {
|
||||
const geojson = JSON.parse(geojsonStr);
|
||||
const coords: number[][] = geojson.features?.[0]?.geometry?.coordinates ?? [];
|
||||
if (coords.length > 0) {
|
||||
return [coords.map((c) => ({ lat: c[1]!, lon: c[0]!, ele: c[2] }))];
|
||||
}
|
||||
} catch { /* invalid geojson */ }
|
||||
return [];
|
||||
}
|
||||
|
||||
function getWaypoints(yjs: YjsState) {
|
||||
return yjs.waypoints.toArray().map((yMap: Y.Map<unknown>) => ({
|
||||
lat: yMap.get("lat") as number,
|
||||
lon: yMap.get("lon") as number,
|
||||
name: yMap.get("name") as string | undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
function getNoGoAreas(yjs: YjsState): NoGoArea[] {
|
||||
return yjs.noGoAreas.toArray().map((yMap: Y.Map<unknown>) => ({
|
||||
points: (yMap.get("points") as Array<{ lat: number; lon: number }>) ?? [],
|
||||
})).filter((a) => a.points.length >= 3);
|
||||
}
|
||||
|
||||
function download(gpx: string, filename: string) {
|
||||
const blob = new Blob([gpx], { type: "application/gpx+xml" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export function ExportButton({ yjs }: { yjs: YjsState }) {
|
||||
const { t } = useTranslation("planner");
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleExport = useCallback(() => {
|
||||
// Get waypoints from Yjs
|
||||
const waypoints = yjs.waypoints.toArray().map((yMap: Y.Map<unknown>) => ({
|
||||
lat: yMap.get("lat") as number,
|
||||
lon: yMap.get("lon") as number,
|
||||
name: yMap.get("name") as string | undefined,
|
||||
}));
|
||||
// Close dropdown on outside click
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
document.addEventListener("click", handler);
|
||||
return () => document.removeEventListener("click", handler);
|
||||
}, [open]);
|
||||
|
||||
// Get route track from GeoJSON
|
||||
let tracks: TrackPoint[][] = [];
|
||||
const geojsonStr = yjs.routeData.get("geojson") as string | undefined;
|
||||
if (geojsonStr) {
|
||||
try {
|
||||
const geojson = JSON.parse(geojsonStr);
|
||||
const coords: number[][] = geojson.features?.[0]?.geometry?.coordinates ?? [];
|
||||
if (coords.length > 0) {
|
||||
tracks = [
|
||||
coords.map((c) => ({
|
||||
lat: c[1]!,
|
||||
lon: c[0]!,
|
||||
ele: c[2],
|
||||
})),
|
||||
];
|
||||
}
|
||||
} catch {
|
||||
// Invalid GeoJSON
|
||||
}
|
||||
}
|
||||
const handleExportRoute = useCallback(() => {
|
||||
const tracks = getTracks(yjs);
|
||||
const gpx = generateGpx({ name: "trails.cool route", waypoints: [], tracks });
|
||||
download(gpx, "route.gpx");
|
||||
setOpen(false);
|
||||
}, [yjs]);
|
||||
|
||||
const gpx = generateGpx({ name: "trails.cool route", waypoints, tracks });
|
||||
|
||||
// Download
|
||||
const blob = new Blob([gpx], { type: "application/gpx+xml" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "route.gpx";
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}, [yjs.waypoints, yjs.routeData]);
|
||||
const handleExportPlan = useCallback(() => {
|
||||
const tracks = getTracks(yjs);
|
||||
const waypoints = getWaypoints(yjs);
|
||||
const noGoAreas = getNoGoAreas(yjs);
|
||||
const gpx = generateGpx({ name: "trails.cool route", waypoints, tracks, noGoAreas });
|
||||
download(gpx, "route-plan.gpx");
|
||||
setOpen(false);
|
||||
}, [yjs]);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className="rounded bg-gray-100 px-3 py-1 text-sm text-gray-700 hover:bg-gray-200"
|
||||
>
|
||||
{t("exportGpx")}
|
||||
</button>
|
||||
<div ref={ref} className="relative">
|
||||
<div className="flex">
|
||||
<button
|
||||
onClick={handleExportRoute}
|
||||
className="rounded-l bg-gray-100 px-3 py-1 text-sm text-gray-700 hover:bg-gray-200"
|
||||
>
|
||||
{t("exportGpx")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="rounded-r border-l border-gray-300 bg-gray-100 px-1.5 py-1 text-sm text-gray-700 hover:bg-gray-200"
|
||||
>
|
||||
▾
|
||||
</button>
|
||||
</div>
|
||||
{open && (
|
||||
<div className="absolute right-0 top-full z-50 mt-1 w-44 rounded border border-gray-200 bg-white py-1 shadow-lg">
|
||||
<button
|
||||
onClick={handleExportRoute}
|
||||
className="block w-full px-3 py-1.5 text-left text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
{t("exportRoute")}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExportPlan}
|
||||
className="block w-full px-3 py-1.5 text-left text-sm text-gray-700 hover:bg-gray-100"
|
||||
>
|
||||
{t("exportPlan")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue