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
+19 -1
View File
@@ -114,6 +114,15 @@ function initMap(mapDiv, stops, legs, kmEls) {
}).addTo(map);
const mid = [(a.lat + b.lat) / 2, (a.lng + b.lng) / 2];
const label = L.marker(mid, { icon: kmLabel(L, leg.km, false, color), interactive: false }).addTo(map);
// Scenic via-points (ground legs only) — small dots in the leg's colour,
// with the waypoint name as a tooltip.
(leg.waypoints || []).forEach((wp) => {
// esc() the name: Leaflet renders a string tooltip as HTML, and wp.name
// is user-typed, so a raw value is a stored-XSS sink for co-travellers.
L.marker([wp.lat, wp.lng], { icon: waypointIcon(L, color), title: wp.name || '' })
.bindTooltip(esc(wp.name || 'Waypoint'))
.addTo(map);
});
return { leg, a, b, line, label, color };
});
@@ -134,7 +143,7 @@ function initMap(mapDiv, stops, legs, kmEls) {
function fetchRoadGeometry(L, map, mapDiv, legLayers, kmEls) {
legLayers.forEach(({ leg, a, b, line, label, color }, i) => {
if (leg.mode !== 'ground') return;
api.directions({ lat: a.lat, lng: a.lng }, { lat: b.lat, lng: b.lng })
api.directions({ lat: a.lat, lng: a.lng }, { lat: b.lat, lng: b.lng }, leg.waypoints || [])
.then((res) => {
// The trip view may have been re-rendered (refreshTrip) while this
// was in flight — the old map/section is detached from the DOM and
@@ -177,6 +186,15 @@ function numberedIcon(L, n, color) {
});
}
function waypointIcon(L, color) {
return L.divIcon({
className: 'wp-dot-wrap',
html: `<span class="wp-dot" style="--wp-color:${color}"></span>`,
iconSize: [10, 10],
iconAnchor: [5, 5],
});
}
function kmLabel(L, km, road, color) {
const accent = color ? ` style="--leg-accent:${color}"` : '';
return L.divIcon({