Skip to content

Self-hosted map tiles with full bikeway rendering - #73

Merged
rodrigohpalmeirim merged 1 commit into
t3code/bike-route-planningfrom
t3code/routing-bikeways-data
Jul 22, 2026
Merged

Self-hosted map tiles with full bikeway rendering#73
rodrigohpalmeirim merged 1 commit into
t3code/bike-route-planningfrom
t3code/routing-bikeways-data

Conversation

@rodrigohpalmeirim

@rodrigohpalmeirim rodrigohpalmeirim commented Jul 11, 2026

Copy link
Copy Markdown
Member

Note

Stacked on #71 (branch includes its commits); retarget to main if #71 merges first, or merge after it.

What

Moves the base map from tiles2.intermodal.pt to our own tile server at https://tiles.gira-mais.app, and fixes the map showing fewer bikeways than the routing service actually uses.

Why

Routing (#71) and the map tiles are both built from OSM, but the standard OpenMapTiles schema drops the cycleway/cycleway:left/cycleway:right tags, so on-road cycle lanes — ~89 km in Lisbon alone, ~50% more than the dedicated cycleways the map showed — were invisible no matter how the style was filtered. The OSRM bicycle profile weights those lanes favourably, so routes kept preferring bikeways the map didn't draw.

Server

  • Two tilesets from the same Geofabrik Portugal extract the routing data uses:
    • /data/v3.json — OpenMapTiles base map built with Planetiler (~400 MB)
    • /data/bikeways.json — 1.3 MB overlay with every way the bicycle profile treats as cycling infrastructure, classified kind=cycleway|lane|shared
  • Fonts (Metropolis + Noto Sans) and the 2-icon sprite are self-hosted too; nothing references Intermodal anymore

App

  • map-style.ts: sources/glyphs/sprite point at TILES_URL; cycleway layers read the bikeways overlay; new dashed bikeway_lane layer for on-road lanes
  • Bikeway layers start at zoom 11 (was 14), fading in at z11–12 with thinner widths, unchanged from z14 up
map map with lower zoom

Test plan

  • bun run check, eslint on touched files
  • Style validates against the MapLibre style spec
  • All public endpoints verified (TileJSON, tiles, fonts, sprite); decoded a live bikeways tile over Avenidas Novas — contains cycleway and lane features
  • Visual check of lane rendering at z11–16 (couldn't browser-test: preview needs re-auth)

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 requested a review from ttmx July 11, 2026 20:18
@rodrigohpalmeirim
rodrigohpalmeirim merged commit 154af79 into t3code/bike-route-planning Jul 22, 2026
2 checks passed
rodrigohpalmeirim added a commit that referenced this pull request Aug 15, 2026
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 added a commit that referenced this pull request Aug 15, 2026
* add route planning with gira bikes and location search

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.

* Keep search UI below the map loading preview

- Add a z-index to contain SearchBar’s stacking context
- Ensure the live map loading preview remains visible above search UI

* Refactor routing.ts, add proper types to osrm

* Add debug mode with walking and fake trip

Fix interaction with destination input pill

* Clip routes on the client side.

* open the station menu automatically at the route's pickup station

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.

* only clip the route along legs the user can be traversing

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).

* defer pin drops past the double-tap window to keep one-finger zoom

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.

* improve trip status metrics formatting and progress tracking

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.

* search for bike stations in the search bar

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.

* show search history and dismiss the search with keyboard or back button

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.

* clear unsubmitted search text when pressing the clear button

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.

* make the keyboard search key select the first result

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.

* fix layout when rotating during a trip and re-lock portrait after trips

* collapse the search bar into a round button during landscape trips

* avoid flashing the fallback name while reverse-geocoding a dropped pin

* cap the search bar width on large screens

* self-host map tiles and render the full bikeway network (#73)

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.

---------

Co-authored-by: ttmx <git@tteles.dev>
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