Add optional route description field

This commit is contained in:
Davide Scaini
2026-05-22 12:06:17 +02:00
parent 67b721d872
commit f20407eecf
2 changed files with 56 additions and 5 deletions
+14
View File
@@ -158,6 +158,7 @@ class PlanBody(BaseModel):
name: str
waypoints: list[dict]
profile: str
description: str | None = None
geojson: dict | None = None
gpxTrack: dict | None = None # full GeoJSON of reference track, if any GPX segments used
gpxColorIdx: int | None = None
@@ -201,6 +202,8 @@ async def create_plan(body: PlanBody, user: User = Depends(require_auth)) -> JSO
"created_at": now,
"updated_at": now,
}
if body.description:
data["description"] = body.description
if body.collection_id:
data["collection_id"] = body.collection_id
if body.dist_km is not None:
@@ -241,6 +244,11 @@ async def update_plan(
"profile": body.profile,
"updated_at": int(time.time()),
})
# description: non-null sets, null clears
if body.description:
data["description"] = body.description
else:
data.pop("description", None)
# collection_id: non-null assigns, null unassigns
if body.collection_id:
data["collection_id"] = body.collection_id
@@ -363,6 +371,8 @@ async def create_shared(body: PlanBody, user: User = Depends(require_auth)) -> J
"created_at": now,
"updated_at": now,
}
if body.description:
data["description"] = body.description
if body.dist_km is not None:
data["dist_km"] = body.dist_km
if body.elevation_gain is not None:
@@ -399,6 +409,10 @@ async def update_shared(
"profile": body.profile,
"updated_at": int(time.time()),
})
if body.description:
data["description"] = body.description
else:
data.pop("description", None)
if body.dist_km is not None:
data["dist_km"] = body.dist_km
if body.elevation_gain is not None:
+42 -5
View File
@@ -22,6 +22,7 @@
let activePlanId = $state(null); // id of currently loaded plan (null = unsaved)
let savePanel = $state(false); // show save form
let saveName = $state('');
let saveDesc = $state('');
let saving = $state(false);
// Collections
@@ -223,7 +224,10 @@
// ── Waypoint management ────────────────────────────────────────────────────
function onMapClick(e) {
if (hoveringGPX && gpxTrack) {
const snapped = nearestOnLine(gpxTrack.geometry.coordinates, [e.lngLat.lng, e.lngLat.lat]);
const prev = lastGpxFrac();
const snapped = prev != null
? nearestOnLineFrom(gpxTrack.geometry.coordinates, [e.lngLat.lng, e.lngLat.lat], prev)
: nearestOnLine(gpxTrack.geometry.coordinates, [e.lngLat.lng, e.lngLat.lat]);
activePlanId = null;
addWaypoint(snapped.lng, snapped.lat, { gpxSnapped: true, gpxFrac: snapped.frac });
return;
@@ -270,7 +274,10 @@
if (gpxFeats.length > 0) {
hoveringGPX = true;
map.getCanvas().style.cursor = 'crosshair';
const snapped = nearestOnLine(gpxTrack.geometry.coordinates, [e.lngLat.lng, e.lngLat.lat]);
const prev = lastGpxFrac();
const snapped = prev != null
? nearestOnLineFrom(gpxTrack.geometry.coordinates, [e.lngLat.lng, e.lngLat.lat], prev)
: nearestOnLine(gpxTrack.geometry.coordinates, [e.lngLat.lng, e.lngLat.lat]);
gpxSnapEl.style.borderColor = gpxColor;
gpxSnapEl.style.background = gpxColor + '40';
gpxSnapMarker.setLngLat([snapped.lng, snapped.lat]).addTo(map);
@@ -297,6 +304,23 @@
return { lng: bestLng, lat: bestLat, frac: bestFrac };
}
// Like nearestOnLine but only searches from fromFrac onwards, so the result
// is always forward along the track relative to the previous gpxSnapped waypoint.
function nearestOnLineFrom(coords, point, fromFrac) {
const start = Math.max(0, Math.floor(fromFrac));
const sub = coords.slice(start);
const result = nearestOnLine(sub, point);
return { ...result, frac: result.frac + start };
}
// Returns the gpxFrac of the last gpxSnapped waypoint, or null if none.
function lastGpxFrac() {
for (let i = waypoints.length - 1; i >= 0; i--) {
if (waypoints[i].gpxSnapped && waypoints[i].gpxFrac != null) return waypoints[i].gpxFrac;
}
return null;
}
function insertionIndex(clickFrac, coords) {
const fracs = waypoints.map(wp => nearestOnLine(coords, [wp.lng, wp.lat]).frac);
for (let i = 0; i < fracs.length - 1; i++) {
@@ -473,9 +497,11 @@
// Resolve each run (Brouter calls run in parallel)
const results = await Promise.all(runs.map(async run => {
if (run.type === 'gpx') {
const snapA = nearestOnLine(gpxTrack.geometry.coordinates, [waypoints[run.from].lng, waypoints[run.from].lat]);
const snapB = nearestOnLine(gpxTrack.geometry.coordinates, [waypoints[run.to].lng, waypoints[run.to].lat]);
return { type: 'gpx', coords: sliceGPXCoords(gpxTrack.geometry.coordinates, snapA.frac, snapB.frac), props: {} };
const fracA = waypoints[run.from].gpxFrac
?? nearestOnLine(gpxTrack.geometry.coordinates, [waypoints[run.from].lng, waypoints[run.from].lat]).frac;
const fracB = waypoints[run.to].gpxFrac
?? nearestOnLine(gpxTrack.geometry.coordinates, [waypoints[run.to].lng, waypoints[run.to].lat]).frac;
return { type: 'gpx', coords: sliceGPXCoords(gpxTrack.geometry.coordinates, fracA, fracB), props: {} };
} else {
const wps = waypoints.slice(run.from, run.to + 1);
const lonlats = wps.map(wp => `${wp.lng.toFixed(6)},${wp.lat.toFixed(6)}`).join('|');
@@ -604,6 +630,7 @@ ${trkpts}
const hasGPXSegs = gpxTrack && waypoints.some(wp => wp.gpxSnapped);
const body = {
name: saveName.trim(),
description: saveDesc.trim() || null,
waypoints: waypoints.map(({ lng, lat, gpxSnapped, gpxFrac }) => ({ lng, lat, gpxSnapped: gpxSnapped ?? false, gpxFrac: gpxFrac ?? null })),
profile,
geojson: route ?? undefined,
@@ -662,6 +689,7 @@ ${trkpts}
profile = plan.profile ?? 'gravel';
activePlanId = plan.id;
saveName = plan.name;
saveDesc = plan.description ?? '';
saveCollId = plan.collection_id ?? '';
saveNewColName = '';
const r = await fetch(`${API}/plans/${plan.id}`, { credentials: 'include' });
@@ -720,6 +748,7 @@ ${trkpts}
clearAll();
profile = plan.profile ?? 'gravel';
saveName = plan.name;
saveDesc = plan.description ?? '';
if (asCopy) {
activePlanId = null;
@@ -776,6 +805,7 @@ ${trkpts}
activePlanShared = false;
saveCollId = '';
saveName = '';
saveDesc = '';
savePanel = false;
saveNewColName = '';
}
@@ -1112,6 +1142,12 @@ ${trkpts}
bind:value={saveName}
onkeydown={(e) => e.key === 'Enter' && saveCollId !== '__new__' && savePlan()}
/>
<textarea
class="save-input save-desc"
placeholder="Description (optional)…"
rows="3"
bind:value={saveDesc}
></textarea>
<select class="save-select" bind:value={saveCollId}>
<optgroup label="Personal">
<option value="">No collection</option>
@@ -1492,6 +1528,7 @@ ${trkpts}
box-sizing: border-box;
}
.save-input:focus { outline: none; border-color: var(--accent); }
.save-desc { resize: vertical; font-family: inherit; line-height: 1.4; }
.full-width { width: 100%; }
.gpx-label { display: block; text-align: center; cursor: pointer; }