Skip to content

feat(roadtrip): a road trip addon with drive-first days and search along the route - #2124

Draft
mauriceboe wants to merge 12 commits into
devfrom
feat/roadtrip-addon
Draft

feat(roadtrip): a road trip addon with drive-first days and search along the route#2124
mauriceboe wants to merge 12 commits into
devfrom
feat/roadtrip-addon

Conversation

@mauriceboe

@mauriceboe mauriceboe commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Description

A road trip addon: a second way to look at a trip, for the case where the driving is the trip.

Switching a trip into road trip mode replaces the day-by-day list with a drive-first view. Each day reads as a chain of stops with the drive between them, and the times cascade the way they actually do on the road: leave at nine, drive two hours, stay ninety minutes, and everything after that moves with it. Anchoring a stop to a fixed time restarts the chain from there, so a ferry or a check-in holds while the rest of the day rearranges around it. The view warns when a day ends after midnight or runs past the last stop's opening time.

Alongside it, search along the route: pick a day, pick what you need (fuel, charging, rest areas, campsites, food, sights) and a corridor width, and it looks along the roads actually driven rather than along the straight lines between stops. That is the difference between finding one petrol station and finding forty. Hits are drawn on the map as suggestions and listed in the right column with how far off the route and how far along it each one is, so "which of these" and "which side of the road" are both answerable. One click adds a hit as a stop on that day.

The drive can be argued with. Dropping a waypoint on a leg pulls it onto another road, and the leg keeps that shape. What is stored is the point where the chosen road differs most from the default, not a polyline, so the choice survives the next OSM update and every stop that moves afterwards. "Other ways" lays the alternatives for one leg over the map with the drive time on each road, placed where that road is unmistakably itself rather than on the stretch they share.

Stops along the way are pauses, not destinations. Everything the corridor search can find sits inside the leg it was found on, carries its own colour and icon, and is left out of every stop count: a day with a charger and a rest stop between four places is a four-stop day, not a six-stop one. How long you stay somewhere is set from the rail and pushes every later arrival back.

Where the work is. Everything above works end to end, but this is a skeleton with real gaps: no mobile road trip surface yet, no range or refuel suggestion, no per-day driving limits, and no wiki page. Opening it as a draft to have somewhere to keep building.

Notable pieces

  • Addon: seeded like every other addon (no migration), gated with @RequireAddon. Nothing changes for an instance that leaves it off.
  • Routing: one request per travel-mode run instead of one per leg, so a ten-stop day is one round-trip against OSRM, not nine.
  • Corridor search: bounding boxes along the routed geometry, then a perpendicular-distance filter. One Overpass query per box covering every selected category, rather than one query per category: same answer, a quarter of the load on a shared mirror, and a quarter of the chances one times out. GET /api/maps/pois and the search_pois MCP tool both take a list now.
  • Waypoints are anchored by position, which means the ground moves under them whenever a day's stops change shape. Inserting, reordering or removing a stop now re-pins every waypoint after it in the same write, in one transaction. When a stop lands in the middle of a bent leg the leg splits in two, and which half each waypoint belongs to is measured on the road actually driven, so adding a petrol station no longer quietly undid the reshaping.
  • Alternatives when the router has none: OSRM answers most long legs with exactly one route, which read as "there is no other way" while a perfectly good slower road existed. The same drive is asked for again without the motorway, and failing that without the tolls, one question at a time. The offer is labelled by what it is rather than by how much slower it is.
  • Car bookings with intermediate stops: the transport form takes waypoints, which is what the discussion asked for. The route, the PDF and the map all read the chain.
  • Map: POI, plugin and route via-point markers position themselves on the map's render clock, the same fix the planned-place pins got in fix(map): position place pins in the map's render cadence on MapLibre #2119. A library marker repositions once per pointer sample while the canvas draws once a frame, so suggestions visibly swam during a drag. Road trip mode does not cluster, so a whole drive stays readable zoomed out, and opening the alternatives frames them.
  • One type scale for both columns, anchored to the day plan's own sizes, so flipping between the two readings of a trip does not change the size of the text beside it.

Two fixes worth calling out separately, both older than this branch:

  • Two of the three assignment loaders built their nested place object by hand and left stop_type out of it while their SQL was already selecting the column. The third copy is gone rather than corrected.
  • The Overpass query carries [timeout:20] and the fetch around it was aborted after twelve seconds, so a mirror under load never got to finish and the corridor reported the stretch as unsearchable. Both budgets now come from one constant.

Related Issue or Discussion

Addresses discussion #1797

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Checklist

  • I have read the Contributing Guidelines
  • My branch is up to date with dev
  • This PR targets the dev branch, not main (wiki-only PRs are exempt)
  • I have tested my changes locally
  • I have added/updated tests that prove my fix is effective or that my feature works
  • I have updated documentation if needed

The trip planner could show one day's route at a time and nothing across days,
so a road trip had to be pieced together in Google Maps (#1797, #435).

The addon adds a mode to the plan view rather than a tab of its own: the map
stays the workspace and gains the route over the whole trip, while the left rail
becomes one chain of stops with the driving distance and time between them and
arrival times that follow from them. A time pinned on a stop is kept and restarts
the chain, so a ferry or a museum ticket stays where the user put it.

Car bookings can finally carry the stops the schema has always allowed: the
endpoints table has held role/sequence for every transport type since #384, but
only flights and trains had an editor for them. The MCP tools already accepted
them, so this closes the gap the other way round.

Legs are fetched one at a time and spaced out — the public routing hosts answer a
whole trip's worth of requests with 429 — and the distance cap now looks at the
single leg instead of the sum, which is what dropped long drives entirely.
The road trip mode could show the route but not what sits beside it, which is
half of what planning a drive is about (#1797).

The right column becomes a corridor search: pick what you need, pick how far off
the route you will go for it, and the day's drive is searched box by box. The
boxes come from the route itself rather than the viewport, so the answer is
"on the way", not "on screen"; each hit reports how far off the route it lies
and how far into the drive it comes, and adding one opens the normal place form
so it lands on the day like any other stop.

Overpass is asked in plain bounding boxes rather than with a polyline filter,
because that is what the existing /api/maps/pois route accepts and what the
mirrors can answer; the geometry work is pure and tested. Long routes stop at ten
boxes and say so instead of quietly searching half the drive.

Fuel, charging, rest areas and campsites join the server's POI categories, so the
explore pill and the MCP search_pois tool gain them too. A car booking's stops now
also reach the PDF, which printed pick-up and return with everything in between
missing.
…e request

Three things the first cut got wrong.

Switching into road trip mode took its time: every leg was its own routing request,
paced a second apart so the shared hosts would answer at all. The router already
returns a leg per consecutive pair of the waypoints it is handed, so a day is now one
request — split only where the travel mode changes, because one request carries one
mode. Answers that come from the cache are no longer paced at all, which is what made
returning to the view feel broken.

A list of petrol stations is half an answer: which one is on your side of the road is
the other half. The corridor search therefore moved out of the panel into the planner,
and its hits are handed to the map like any other place suggestion — grey, because they
are not part of the trip until you add one. The explore pill stands down while the mode
is on: it draws into the same channel and answers the same question for a drive.

The panel itself was a stack of controls without an order. It now reads as header,
search, results: what is being searched (with the day's distance and driving time),
what is being looked for, how far off the route, then the hits grouped by kind with a
count — and a progress bar while the boxes are being worked through.
…tops

The corridor was built from the day's stops, so it followed straight lines across
country the car never drives. Between two cities that line runs through open fields
while the motorway swings wide of it, which is why every hit came back sitting on a
stop rather than on the way.

The days now carry the routed geometry, and the corridor is cut from that. Same trip,
same search: 192 hits spread over the whole 482 km — first fifty, middle, and the last
stretch — where before all of them reported kilometre zero.

Following real roads takes more boxes to cover the same drive, so they go out three at
a time instead of strictly one after another, and the budget rises from ten to sixteen.
The panel head loses the sentence explaining what the sections below already say, and
the day's distance and driving time, which the day list has been showing all along.
…a stretch went unsearched

A petrol station and a campsite both came back as the same nameless grey dot, because
the road categories only existed on the server: the map falls back to grey for a
category it does not know. They now sit in the client's category table too — with the
pump, the bolt, the parking sign and the tent, in their own colours — which both map
renderers and the popup read from the same place.

They stay out of the explore pill's own list on purpose: that pill offers what a city
visit needs, and while road trip mode is on it is not on screen anyway.

The search also stayed quiet when only some boxes failed, which reads as "no fuel on
this stretch" when the truth is that nobody looked. A partial failure now says how many
stretches went unanswered.
A corridor full of petrol stations was a row of identical coloured dots, and the one
thing that tells them apart is the brand. OSM carries brand:wikidata on most of them —
88% of the fuel stations in a Berlin box — Wikidata carries the logo as property P154,
and Commons serves it.

The bytes are proxied and cached rather than linked: one self-hosted instance asking
Wikimedia for a handful of logos is a very different egress profile from every
visitor's browser announcing which petrol stations they are looking at. The cache is in
memory because a GET must not write to the database, and there are only so many fuel
brands; a brand with no logo is remembered too, or every pan would ask again.

A logo-less or unreachable brand answers 204 and the pin keeps its category icon, so
nothing depends on Wikidata being reachable. Both renderers show it, since both read
the same category table.
…egory

The road trip corridor asks for fuel, charging, rest areas and campsites over a
dozen boxes, and asking per category was four times the requests for the same
answer — slow, and four times the chance a shared Overpass mirror times out and
reports the stretch as unsearched.

`category` now takes a comma-separated list. Each OSM selector belongs to exactly
one category, so a hit is still labelled with the kind it answered, and the
result cap is shared out per category so one dense kind can't crowd the rest.
The MCP tool takes an array for the same reason.

Also puts the POI, plugin and route via-point pins on the map's render clock,
the same fix the planned-place pins already had: a library marker repositions on
every pointer sample while the canvas draws once a frame, so the suggestions
swam during a drag and snapped back on release.
The addon shipped with only its two pure modules under test, which left the parts
that actually talk to the network — and the panel a user reads the answer from —
resting on nothing.

Covers the routing hook (one request per travel-mode run, day default over trip
fallback, retry then give up, no re-fetch on a rename, abort on leave), the
corridor search (one query per box carrying every kind, the perpendicular filter,
dedupe, the retry pass before a stretch counts as unsearched), the corridor state
(roads driven over the line between stops, day follow-along) and both panels.

Two findings from writing them, fixed here:

- A dead straight leg is reduced to its two ends by the simplifier, and tiling
  walks point to point — so a 250 km motorway stretch produced a box at each end
  and nothing in between, while the search still reported itself complete. Steps
  longer than half a box are now split before tiling.
- projectOnSegment guarded a degenerate segment with `lenSq === 0`, which two
  ends that came through a chain of floating point arithmetic never hit.

Also splits the anchor resolution out of computeSchedule, which was three levels
of nesting for one question, and sorts the POI cache key with a comparator.
The client's lib is ES2020, where Array#at does not exist. Local typecheck ran
before the file was added and CI caught it.
@sonarqubecloud

Copy link
Copy Markdown

Three read paths built the same nested place object by hand, and two of them
left `stop_type` out of it while their SQL was already selecting the column.
The effect was visible: a petrol station added along a route drew correctly
for a moment from the optimistic write, then the server's answer replaced it
with a place that had no kind, and the road-trip rail redrew it as an ordinary
numbered destination.

`getAssignmentWithPlace` now goes through `formatAssignmentWithPlace` like the
list path already did, which removes the third copy rather than adding the
missing field to it. The two queries in the days service gain the column, and
its hand-built shape gains the field it cannot yet be spared.
The query carries `[timeout:20]`, so the mirror is allowed twenty seconds of
work on it — and the fetch around it was aborted after twelve. A mirror under
load that would have answered at fifteen never got the chance, and the caller
saw a failed area.

That is what makes a corridor search along a long drive report half its
stretches as unsearchable: the stretches were being searched, we just hung up.
Both numbers now come from one constant, the client budget is the query's own
plus room to hand the answer back, and a test holds the relationship rather
than the two literals.
…the way

The road-trip addon's second half: the parts that let a planned drive be
argued with rather than only read.

Reshaping. A via records which stop it follows by position, and that position
is resolved fresh every time the day is routed — so inserting, reordering or
removing a stop moved the ground under every via after it, and the drive
silently snapped back to the road it had been steered away from while the
handle still sat on the map where it was left. The correction is computed
against the day as it stands and written with the same change, in one
transaction: a half-applied re-anchoring mixes two numbering schemes in one
day. The one judgement that needs geometry — which half of a split leg a via
belongs to when a stop lands in the middle of it — is measured on the road
actually driven, so a fuel stop added to a bent leg keeps the bend on the side
it belongs to.

Alternatives. OSRM answers most long legs with exactly one route, which read as
"there is no other way" when a perfectly good slower road existed. When it
offers nothing, the same drive is asked for again without the motorway, and
failing that without the tolls — one question at a time, stopping as soon as
one lands. The offer says what it is rather than how much slower.

Stops along the way. Everything the corridor search can find is a pause, not a
destination: fuel, charging, rest areas, campsites, food and sights all sit
inside the leg they were found on, carry their own colour and icon, and are
left out of every stop count. That colour is defined once and read by the rail,
the corridor list, the map pin and the picker — and it replaced the chains'
logos, which turned a corridor full of petrol stations into a row of
advertisements and left the brands with no logo on file looking like something
else entirely.

How long a stop takes was readable in the rail and writable nowhere; new places
were given an invented hour that nobody had entered. It is now set from the
rail, cleared as a zero because the update statement folds a null into "leave
it alone", and new places start with nothing.

Both columns are rebuilt on one type scale, anchored to the day plan's own
sizes so switching tabs does not change the size of the text beside it. The
rail reads as trip totals, day cards and a chain of stops with the drives
between them; the corridor column as what to look for, how the search is
getting on, and what it found. Six category pills over three lines became one
dropdown, which is the space the results wanted.

Also: the offered routes are framed on the map when the picker opens (`fitKey`
carries no coordinates and each renderer reads it as "the selected day"), road
trip mode does not cluster at all so the whole drive stays readable zoomed out,
and the drive-time labels finally render in the app's own typeface instead of
Leaflet's Helvetica. All of it in both renderers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant