trails/apps/planner/app/components/SaveToJournalButton.tsx
Ullrich Schäfer ef85e03bb2
Export only computed track, not editing waypoints
Editing waypoints represent where the user clicked, not the actual
route. Exporting them causes reimported routes to look different
because BRouter re-routes without no-go areas.

Now both GPX export and Save to Journal only include the computed
track. On reimport, Douglas-Peucker extracts waypoints from the
track shape, preserving the route including no-go detours.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:46:12 +01:00

83 lines
2.7 KiB
TypeScript

import { useState, useCallback } 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";
interface SaveToJournalButtonProps {
yjs: YjsState;
callbackUrl: string;
callbackToken: string;
returnUrl?: string;
}
export function SaveToJournalButton({ yjs, callbackUrl, callbackToken, returnUrl }: SaveToJournalButtonProps) {
const { t } = useTranslation("planner");
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSave = useCallback(async () => {
setSaving(true);
setError(null);
try {
// Build GPX from computed track (not editing waypoints).
// The track reflects the actual routed path including no-go avoidance.
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 gpx = generateGpx({ name: "trails.cool route", waypoints: [], tracks });
// POST to Journal callback
const response = await fetch(callbackUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${callbackToken}`,
},
body: JSON.stringify({ gpx }),
});
if (!response.ok) {
const result = await response.json();
throw new Error(result.error ?? "Save failed");
}
setSaved(true);
} catch (err) {
setError((err as Error).message);
} finally {
setSaving(false);
}
}, [yjs, callbackUrl, callbackToken]);
return (
<div className="flex items-center gap-2">
<button
onClick={handleSave}
disabled={saving}
className="rounded bg-green-600 px-3 py-1 text-sm text-white hover:bg-green-700 disabled:opacity-50"
>
{saving ? t("saving") : t("saveToJournal")}
</button>
{saved && <span className="text-xs text-green-600">{t("saved")}</span>}
{error && <span className="text-xs text-red-600">{error}</span>}
{saved && returnUrl && (
<a href={returnUrl} className="rounded bg-gray-100 px-2 py-1 text-xs text-gray-700 hover:bg-gray-200">
{t("returnToJournal")}
</a>
)}
</div>
);
}