This document describes how bicycle routes between Oslo Bysykkel stations are fetched, cached, exported, and displayed.
flowchart LR
subgraph local [Local Pipeline]
NB1[stations_prepare.ipynb]
NB2[google_routes_test.ipynb]
RF[routes_fetch.py]
end
subgraph committed [Committed]
RC[routes-cache/single/]
end
subgraph build [Build Time]
Export[export-routes-to-prepared.js]
end
subgraph artifacts [Build Artifacts]
Routes[prepared-data/routes.json]
end
subgraph frontend [Frontend]
RT[route-test.astro]
end
NB1 --> Stations[stations.json]
NB2 --> RF
RF --> RC
RC --> Export
Export --> Routes
Routes --> RT
stations_prepare.ipynbextracts unique stations with lat/lon and trip summaries and writesprepared-data/stations.json.google_routes_test.ipynbcallsroutes_fetch.fetch_route()to request bicycle routes from Google Routes.- Full API responses are stored in
routes-cache/single/{origin_id}_{dest_id}.json(committed cache). export-routes-to-prepared.jsreads cache files and writes slim data toprepared-data/routes.json.npm run prepare:datacopiesprepared-datainto the frontend./route-test/loads stations and routes and renders them with Leaflet.
Location: data-pipeline/routes_fetch.py
This module fetches bicycle routes through Google Routes API REST. It checks cache files first, and it supports forced refetch through force_fetch=True or FORCE_ROUTES_FETCH=1.
Available functions:
fetch_route(origin_id, dest_id, stations, api_key, cache_dir, force_fetch=False)fetch_routes_batch(route_pairs, stations, api_key, cache_dir, force_fetch=False)
The code uses Compute Routes (one origin and one destination per request), not Compute Route Matrix. Batch fetch loops over route pairs and still sends one request per pair.
The field mask is limited to routes.duration, routes.distanceMeters, and routes.polyline to keep response size low.
Each cached file contains:
{
"origin_id": "377",
"dest_id": "381",
"fetched_at": "2026-02-27T19:34:05.592478",
"response": { /* full Google Routes API response */ }
}With the minimal field mask, the cached response contains only duration, distanceMeters, and polyline. Size: ~1–3 KB per route (vs ~12 KB with full fields).
Location: scripts/export-routes-to-prepared.js
The script reads routes-cache/single/*.json and writes prepared-data/routes.json.
Each route in output has origin_id, dest_id, duration_sec, distance_m, and encodedPolyline.
Output size is around 400 bytes per route, compared to 1 to 3 KB in cache with the minimal mask.
{
"last_export": { "timestamp": "2026-02-27T18:39:07.851Z" },
"data": {
"routes": [
{
"origin_id": "377",
"dest_id": "381",
"duration_sec": 184,
"distance_m": 1282,
"encodedPolyline": "mgulJ__x`ATpCD`@..."
}
]
}
}Location: frontend/src/pages/route-test.astro
The page is available at /route-test/. It loads stations.json (or isochrones.json) and routes.json, then draws markers and route lines with Leaflet.
A route dropdown controls the selected route, and the map fits bounds for that route.
The fetch sends a minimal request body:
| Option | Value | Notes |
|---|---|---|
origin |
{ location: { latLng: { latitude, longitude } } } |
Station coordinates |
destination |
Same structure | Station coordinates |
travelMode |
"BICYCLE" |
Essentials SKU, beta for cycling paths |
units |
"METRIC" |
Distance in metres |
The request does not send routingPreference, departureTime, routeModifiers, or computeAlternativeRoutes.
- One route per request (no alternatives).
- Duration in seconds (for example
"184s"). - Distance in meters.
- Encoded route polyline.
- Route legs are not requested.
No shortest-distance option: the API returns the route it considers best for bicycle travel, not strictly shortest distance.
There is no parameter for "shortest distance" in BICYCLE mode.
Not time-dependent: bicycle routes are static. routingPreference options apply to DRIVE and TWO_WHEELER, not BICYCLE.
No historical bicycle routes: past departure times are supported for TRANSIT, not BICYCLE.
| Format | Per route | 85k routes (full matrix) |
|---|---|---|
| Cache (minimal mask) | ~1–3 KB | ~85–255 MB |
| Slim (routes.json) | ~400 bytes | ~25 MB |
routes-cache/is committed and used as source data for routes.prepared-data/routes.jsonis generated from cache and is not committed.
The field mask is trimmed to the minimum required for slim format (routes.duration, routes.distanceMeters, routes.polyline). Omitting viewport, legs, and travelAdvisory reduces server processing and response size, which can lower per-request cost. The main cost lever remains the number of requests; consider fetching only observed station pairs from trip data instead of the full matrix.
A medium format (routes_medium.json) was briefly implemented and then removed. It stored per-leg data (route origin + legs with end_lat, end_lon, distance_m, duration_sec, encodedPolyline per leg) to support multi-leg routes.
Why it was removed: station-to-station bicycle routes in this project return one leg (origin to destination, no waypoints). The medium format added file size and maintenance work with no gain.
Do not reintroduce it unless request requirements change. The current field mask does not request legs.
| Command | Description |
|---|---|
npm run prepare:routes |
Export routes from cache to prepared-data only |
npm run prepare:data |
Export routes + sync prepared-data to frontend |
npm run build |
Runs prepare:data, then builds the frontend |
- GOOGLE_ROUTES_API_KEY – Required for fetching; set in
.env(see.env.example) - FORCE_ROUTES_FETCH – Set to
1to bypass cache during development
- If routes are missing in the frontend, run
npm run prepare:dataand checkprepared-data/routes.json. - If route fetch fails in notebooks, check
GOOGLE_ROUTES_API_KEYin.env. - If cache data is stale, set
FORCE_ROUTES_FETCH=1and rerun fetch for target pairs.
stations_prepare.ipynbbuildsstations.jsonwith station id, name, lat, lon, and trip counts.google_routes_test.ipynbtests single and batch route fetch. The batch workflow uses top stations and top connections, and only uncached pairs trigger API requests.