Skip to content

Route planning with GIRA bikes and location search - #71

Merged
rodrigohpalmeirim merged 18 commits into
mainfrom
t3code/bike-route-planning
Aug 15, 2026
Merged

Route planning with GIRA bikes and location search#71
rodrigohpalmeirim merged 18 commits into
mainfrom
t3code/bike-route-planning

Conversation

@rodrigohpalmeirim

@rodrigohpalmeirim rodrigohpalmeirim commented Jul 10, 2026

Copy link
Copy Markdown
Member

What

Adds multimodal route planning to the app: search for a destination (or drop a pin on the map, or tap a station) and instantly see the best route from your current location, combining walking and GIRA bikes.

https://routing.gira-mais.app (self-hosted on gira-mais-vps):

  • OSRM ×2 over a greater-Lisbon extract — default foot profile, plus a bicycle profile using cyclability weighting with an extra 1.2× bonus for dedicated cycling infrastructure, so bike legs prefer bikeways while walking legs don't care
  • Photon geocoder (Portugal dump) for search + reverse geocoding, biased to the user's location and limited to the covered bbox

Route planning (src/lib/routing.ts)

  • No trip: walk → station with bikes → bike → station with free docks → walk, choosing among 3 candidate stations on each side via OSRM table calls (with pickup/docking time overheads), compared against walking directly — whichever is faster wins
  • On a trip: bike from current position → best dock near destination → walk; populates the previously unused destination/distanceLeft/arrivalTime fields of ActiveTrip, lighting up the existing TripStatus metrics
  • Station destinations end at the station itself
  • Routes recompute as you move (>30 m, throttled), on trip start/end, and clear on arrival
  • Transient server failures (5xx/network) are retried and never mistaken for "no route": a failed request can't silently drop the walk-only comparison and mislead into a bike route

UI

  • Floating search bar with debounced Photon autocomplete; the results list and the route summary (per-leg minutes + arrival time) live in a drawer that extends from behind the bar, morphing its height between the two
  • Route drawn on the map: solid line for bike legs, dotted for walking, destination dot; camera fits the route once per destination, accounting for MapLibre's persistent-padding quirk and re-fitting when the station sheet resizes
  • Tapping a station (bikes or docks view) makes it the destination; tapping elsewhere drops a pin (reverse-geocoded); Android back clears the route
  • Trip rating card moved below the search bar; route summary hidden during trips (TripStatus already shows it)

Tests & tooling

  • Integration tests for computeRoute against the live routing server (multimodal legs, empty-station avoidance, walk-only shortcut, on-trip routing, station destinations, mid-computation destination rename race)
  • Error-handling tests with a mocked HTTP layer (persistent 502s, transient 502 recovery, no-retry on legitimate NoRoute)
  • Upgrades vitest 1.x → 3 (was incompatible with vite 7; the suite couldn't run at all) with happy-dom as test environment

Server notes

Load-tested: ~900 req/s routing capacity vs ~75 req/s modeled worst-case peak (from collected usage data); Photon is the soft spot at ~45 req/s vs <5 req/s needed.

Test plan

  • bun run check, lint on touched files, bunx vitest run (10 tests)
  • Browser-verified flows: search → route, pin drop → reverse-geocoded route, station tap → route + sheet, re-selection camera fit, trip HUD metrics, drawer animations, dark mode
  • On-device testing (Android/iOS builds)

@rodrigohpalmeirim
rodrigohpalmeirim force-pushed the t3code/bike-route-planning branch from 831af9a to 0ad825a Compare July 17, 2026 19:43
@rodrigohpalmeirim rodrigohpalmeirim mentioned this pull request Jul 24, 2026
3 tasks
@rodrigohpalmeirim
rodrigohpalmeirim marked this pull request as ready for review August 15, 2026 18:51
rodrigohpalmeirim and others added 18 commits August 15, 2026 19:57
Search for a destination, drop a pin on the map, or select a station, and
instantly see the best route from the current location: walk to a station
with bikes, ride to a dock near the destination (prioritizing bikeways),
walk the rest — or walk directly when that's faster. During a trip, routes
go from the current position to the best dock and feed the trip HUD's
destination metrics (distance left, time left, arrival).

Served by self-hosted OSRM instances (foot profile + bicycle profile with
cyclability weighting and a dedicated-cycleway bonus) and a Photon geocoder
behind routing.gira-mais.app, covering the greater Lisbon area.

Also upgrades vitest so the test suite runs again (vitest 1.x was
incompatible with vite 7), using happy-dom as the test environment.
- Add a z-index to contain SearchBar’s stacking context
- Ensure the live map loading preview remains visible above search UI
Fix interaction with destination input pill
When a route involves picking up a bike (or leads to a station), the
station menu now opens on its own once the user gets within unlocking
distance of that station, so unlocking a bike doesn't require tapping
it — which used to replace the route. It opens once per approach, so
dismissing it sticks.

Tapping a station that is already part of the route (pickup, dropoff or
the station destination) now only opens its menu and centers it, instead
of rerouting to that station.
Route clipping projected the position onto the whole route, so walking
across the bike leg on the way to the pickup station (or riding across
the final walking leg) would clip sections that weren't traversed yet.
The projection is now restricted to the leg of the current progress plus
consecutive legs matching the current travel mode (foot without a trip,
bike during one).
Tapping the map to set a destination broke MapLibre's double-tap and
double-tap-drag zoom gestures, since 'click' also fires on their first
tap. Dropping the pin is now deferred by 200 ms and cancelled when a
gesture materializes (dblclick, zoom, drag, rotate or pitch start).

The route request still starts at the moment of the tap — only showing
it is deferred — so the pin usually appears together with its route and
the total time to see a route is unchanged.
Distance left now shows meters under 1 km like distance traveled, and
time left is rounded to minutes ("26min", "1h26min", "<1min") with the
hour unit styled like the minute one.

Both distance left and arrival time are also refined with the clipped
progress along the route on every position update, instead of stepping
only when a recompute lands every ~30 m.
Stations are matched locally (case- and accent-insensitive, by name or
station number) and shown at the top of the results instantly, with live
bike/dock availability, while geocoder results follow. Selecting one
behaves like tapping the station on the map: the route ends at the
station and its menu opens.
Focusing the search bar with an empty query now shows the last ten
search selections, persisted across sessions; station entries resolve
against the live station list and fall back to a plain location when the
station is gone.

Dismissing the keyboard (back press, done key or swipe) also unfocuses
the search bar, closing the results — via the native keyboard event, so
focusing the bar cannot re-trigger it. The back button handler dismisses
the search first as a backstop when the keyboard is already closed.
Clearing relied on the routeDestination subscription resetting the
query, but stores skip notifying when the value is unchanged — with no
destination selected the store is already null, so typed text survived
the clear button. Reset the query and results directly.
The search key picks the first visible row: the top station match if
any, else the top geocoder result. When the debounced search hasn't
returned yet, it runs immediately and the top hit is selected when it
arrives.
The map style now points at tiles.gira-mais.app (tileserver-gl on
gira-mais-vps) instead of tiles2.intermodal.pt, and draws bikeways from
a dedicated tileset built from the same Geofabrik Portugal extract the
routing server uses, so displayed bikeways always match what routing
prefers.

The standard OpenMapTiles schema drops cycleway:left/right tags, so
on-road cycle lanes (~89 km in Lisbon) could never be rendered from the
old tiles. The new bikeways tileset classifies every way the OSRM
bicycle profile favours into kind=cycleway|lane|shared; lanes render
dashed, and all bikeway layers now start at zoom 11 instead of 14.
@rodrigohpalmeirim
rodrigohpalmeirim force-pushed the t3code/bike-route-planning branch from 154af79 to a4e3c91 Compare August 15, 2026 18:57
@rodrigohpalmeirim
rodrigohpalmeirim merged commit 99163f7 into main Aug 15, 2026
2 checks passed
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.

2 participants