Montréal's transit network, drawn as the trails its vehicles leave behind.
Greater Montréal during the morning peak, 08:00–09:00, with buses, métro, REM, commuter trains, and aircraft replayed as animated trails. Colour identifies the mode; brighter trails indicate higher speeds.
Named after sillage, the French word for the wake a boat leaves on water, Sillage records live positions from STM buses and OpenSky aircraft in a PostGIS spatiotemporal database, simulates métro, commuter rail, and REM movement from static GTFS schedules, and replays them together as animated trails using deck.gl and MapLibre.
The published demo uses curated, precomputed scenes and requires no backend. Run the project locally to query arbitrary time windows directly from the database.
Two Python fetchers poll public APIs and write each position fix to PostgreSQL. A FastAPI backend retrieves a requested time window, combines the recorded positions with schedule-simulated modes on a shared timeline, and returns the paths and timestamps expected by deck.gl's TripsLayer.
The browser then replays that window interactively: scrub through time, change playback speed, adjust trail persistence, and toggle individual modes.
Because positions are stored rather than only streamed, any collected period can be replayed later.
| Layer | Choice |
|---|---|
| Ingestion | Python, gtfs-realtime-bindings, opensky-api |
| Storage | PostgreSQL 17, PostGIS 3.6 |
| API | FastAPI, uvicorn |
| Simulation | Static GTFS interpolation using shapes.txt and stop_times.txt |
| Map | MapLibre GL JS |
| Layers | deck.gl TripsLayer and ScatterplotLayer |
| Basemap | CARTO Dark Matter |
- PostgreSQL 17 with PostGIS (
brew install postgresql@17 postgis) - Python 3.12
- An STM API key from the STM developer portal
- An OpenSky OAuth2 client. Anonymous OpenSky access is capped at 400 requests per day, which is insufficient for sustained 20-second polling.
git clone https://github.com/kyuchia/sillage.git
cd sillage
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
createdb sillage
psql sillage < db/schema.sqlSecrets are stored in the macOS Keychain rather than in files or launchd property lists:
security add-generic-password -a "$USER" -s sillage-stm -T /usr/bin/security -U -w
./scripts/store_opensky_credentials.sh ~/Downloads/credentials.jsonpython fetchers/stm_fetcher.py # buses, every 20s
python fetchers/opensky_fetcher.py # aircraft, every 20sAn hour of morning rush-hour collection typically yields around 1,200 vehicles and 180,000 position fixes.
For unattended collection, launchd agents can run the fetchers independently of a terminal session and keep the machine awake for the lifetime of each process:
sudo pmset -c sleep 0 disksleep 0 # AC only; battery behaviour is unchanged
cp launchd/ca.sillage.*.plist ~/Library/LaunchAgents/
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/ca.sillage.stm.plist
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/ca.sillage.opensky.plistFetch and archive the feeds used by the simulated modes:
python scripts/fetch_gtfs.pyuvicorn api.main:app --reload --port 8000Open http://localhost:8000/. FastAPI serves the visualization directly.
URL parameters are forwarded to the API:
/?layer=all&start=2026-08-20%2008:00-04:00&end=2026-08-20%2009:00-04:00
layer accepts bus, aircraft, metro, train, rem, the aliases both and all, or a comma-separated list such as metro,rem.
GitHub Pages serves docs/ statically, so the published site cannot query the PostgreSQL database or FastAPI backend directly. Instead, selected time windows are exported into docs/scenes/ and made available through a scene picker.
To rebuild them:
uvicorn api.main:app --port 8000 &
./scripts/bake_scenes.shWhen running locally, the visualization prefers the live API. Baked scenes act as a fallback, and the interface indicates when fallback data is being shown.
Recorded positions are stored in two tables. Each includes a GENERATED PostGIS geography column derived from latitude and longitude, a GiST index for spatial queries, and a B-tree index on fetched_at for time-range scans.
See db/schema.sql for the full schema.
For example, buses within 500 metres of Place-des-Arts during the last hour can be grouped by route with:
SELECT route_id, COUNT(DISTINCT vehicle_id)
FROM vehicle_positions
WHERE fetched_at > NOW() - INTERVAL '1 hour'
AND ST_DWithin(
geom,
ST_MakePoint(-73.5772, 45.5048)::geography,
500
)
GROUP BY route_id
ORDER BY 2 DESC;TripsLayer represents each vehicle as one record containing parallel path and timestamps arrays. Sillage uses timestamps in seconds relative to the beginning of the requested window rather than Unix epoch values, avoiding unnecessary precision loss during deck.gl interpolation.
TripsLayer renders the trail but not a moving vehicle head. To show current positions, the client binary-searches each vehicle's timestamp array, finds the two fixes surrounding the current playback time, and interpolates between their coordinates. The resulting positions are rendered separately with ScatterplotLayer.
Hue identifies mode; brightness encodes speed. Per-route colouring quickly becomes rainbow soup across more than 200 bus routes, so colour is assigned by mode instead. Official colours are preserved where available, including STM's four métro lines and REM's #73A400.
scripts/check_colours.js validates the resulting ramps with CIEDE2000 and fails the build when colours become too perceptually similar.
STM does not publish shape_dist_traveled, so stops must be projected onto route geometry. A simple nearest-point search can place a later stop earlier along a shape where the route passes near itself, so projections are constrained to move forward.
GTFS times can also extend past midnight. A departure at 25:30:00, for example, belongs to the previous service day even though it occurs on the next calendar day.
Finally, static GTFS feeds cover limited service periods. Historical recordings are therefore matched to a compatible service date when the original date falls outside the available feed.
| Mode | Source | Rendering | Terms |
|---|---|---|---|
| STM bus | STM GTFS-Realtime | Recorded | CC BY 4.0 |
| Aircraft | The OpenSky Network OAuth2 API | Recorded | See terms |
| STM métro | STM static GTFS | Schedule-simulated | CC BY 4.0 |
| REM | CDPQ Infra static GTFS | Schedule-simulated | CC BY 4.0 |
| exo commuter rail | exo / ARTM static GTFS | Schedule-simulated | CC BY |
| RTL / STL | GTFS-Realtime, application required | Not implemented | — |
Basemap © CARTO, © OpenStreetMap contributors.
STM métro and REM do not provide realtime vehicle positions, while exo realtime access requires a separate application. These modes therefore use static GTFS schedules.
Schäfer, Matthias, Martin Strohmeier, Vincent Lenders, Ivan Martinovic, and Matthias Wilhelm. 2014. "Bringing Up OpenSky: A Large-scale ADS-B Sensor Network for Research." In Proceedings of the 13th IEEE/ACM International Symposium on Information Processing in Sensor Networks (IPSN).
The source code for Sillage is released under the MIT License.
The baked scenes in docs/scenes/ contain derived transit and aircraft data and are not covered by the MIT License. They remain subject to the terms of their respective providers listed under Data sources.
