Add scenic waypoints for drive legs (OSRM via-routing)

- Transport entries carry an optional ordered waypoints array of
  lat/lng/name points; /route attaches them to the ground leg the
  transport bridges, and /api/directions accepts a via param so the
  drawn road route detours through them
- Day editor gains a geocoded "Scenic waypoints" list on transport
  entries; the map draws leg-coloured waypoint dots
- Escape waypoint names in the Leaflet tooltip (stored-XSS fix flagged
  by security review: names are user-typed and Leaflet renders string
  tooltips as HTML)
This commit is contained in:
2026-07-20 14:57:26 +07:00
parent 9369c82e56
commit e2c3089c25
15 changed files with 594 additions and 16 deletions
+13 -1
View File
@@ -22,6 +22,7 @@ import {
} from '../format.js';
import { createFlightRoute, renderSegmentLines } from './segments.js';
import { createRentalDetails, renderRentalLine } from './rental.js';
import { createWaypoints } from './waypoints.js';
import { createCostForm } from './costForm.js';
import { enableRowReorder } from './dragdrop.js';
@@ -190,6 +191,7 @@ export function openDayEditor(tctx, date) {
// Flight segments (load() triggers the flight-route onChange -> UI sync).
fields.flightRoute.load(Array.isArray(entry.segments) ? entry.segments : []);
fields.rentalDetails.load(entry.rental || null);
fields.waypoints.load(Array.isArray(entry.waypoints) ? entry.waypoints : []);
renderLoc();
panel.querySelector('.entry-form').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
fields.title.focus();
@@ -243,6 +245,10 @@ export function openDayEditor(tctx, date) {
// state), before the section elements below exist — gate until wired.
let typeUIReady = false;
const flightRoute = createFlightRoute({ onChange: () => { if (typeUIReady) syncTypeUI(); } });
// ----- Scenic waypoints subsection (shown only for transport entries) -----
const waypoints = createWaypoints({ onChange: () => { if (typeUIReady) syncTypeUI(); } });
const waypointsSection = waypoints.node;
const flightSection = el(
'div',
{ class: 'flight-section' },
@@ -269,6 +275,7 @@ export function openDayEditor(tctx, date) {
flightSection.style.display = type === 'flight' ? '' : 'none';
rentalSection.style.display = type === 'rental' ? '' : 'none';
modeField.style.display = type === 'transport' ? '' : 'none';
waypointsSection.style.display = type === 'transport' ? '' : 'none';
locationField.style.display = type === 'flight' && flightRoute.hasSegments() ? 'none' : '';
// Stays emphasise an end date ("until"); other types call it "End date".
endDateLabel.textContent = type === 'stay' ? 'Until' : 'End date';
@@ -285,7 +292,7 @@ export function openDayEditor(tctx, date) {
type: typeSelect, title: titleInput, details: detailsInput, start: startInput, end: endInput,
endDate: endDateInput, mode: modeSelect,
locInput, locResults, locSelected,
cost: costForm, flightRoute, rentalDetails,
cost: costForm, flightRoute, rentalDetails, waypoints,
};
wireGeocode(locInput, locResults);
@@ -354,6 +361,10 @@ export function openDayEditor(tctx, date) {
// changing an entry's type away from transport drops any prior mode.
payload.transport_mode = typeSelect.value === 'transport' ? (modeSelect.value || null) : null;
// Scenic waypoints (transport entries only). Clear them otherwise so
// changing an entry's type away from transport drops any prior waypoints.
payload.waypoints = typeSelect.value === 'transport' ? waypoints.read().waypoints : null;
// end_date must be on/after the effective start date (rental may have
// moved payload.date to the pickup date above).
if (payload.end_date && payload.end_date < payload.date) {
@@ -416,6 +427,7 @@ export function openDayEditor(tctx, date) {
),
flightSection,
rentalSection,
waypointsSection,
locationField,
el('div', { class: 'cost-heading' }, 'Cost (optional)'),
costForm.node,