From b8ccfd1648755d31d18644cbeec9d7768ffedb0a Mon Sep 17 00:00:00 2001 From: Cliff Flood Date: Wed, 16 Sep 2026 20:22:45 -0400 Subject: [PATCH] feat(bikeshare): add Bike Share Toronto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toronto runs the same PBSC platform as the Austin, Honolulu and Chattanooga systems already in the registry, and publishes the identical GBFS v2 endpoint pair under `toronto.publicbikesystem.net`. The proxy allowlist already admits the host through its `*.publicbikesystem.net` wildcard and the paths end in the expected `station_information.json` / `station_status.json`, so the system needs no new plumbing — only the registry entry that names it. Verified against the live feeds: 1,069 stations in both documents, all 1,069 joining on `station_id`. The layer reads `num_bikes_available`, which Toronto populates normally, so availability renders without a vendor-specific mapping. It does not populate the standard `num_bikes_available_types`, reporting per-type counts under PBSC ids (ICONIC/BOOST/EFIT/ASTRO) instead; nothing reads that field today, and this entry deliberately does not translate it rather than adding a mapping with no consumer. The center is downtown at 43.6532, -79.3832 with the group's 90 km load radius. The stations span roughly 37 km east-west, from Etobicoke to Scarborough, so a tighter radius would drop the outer ends on a downtown pan. The new registry test is the reason this was cheap to be sure about: it holds every city's feed URLs against the same host and path predicates the proxy enforces, so a city the proxy would silently refuse now fails the suite instead of rendering an empty layer. Credited to Bike Share Toronto (Toronto Parking Authority). The City's open-data package declares no licence and the feed publishes no `license_url`, so it is treated as attribution-only, like the other GBFS operators, rather than claiming a licence that was never granted. --- CHANGELOG.md | 6 +++ DATA_SOURCES.md | 1 + docs/CURRENT-STATE.md | 2 +- src/data/dataCredits.js | 4 +- src/layers/bikeshare/registry.js | 12 ++++++ src/layers/bikeshare/registry.test.mjs | 58 ++++++++++++++++++++++++++ 6 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/layers/bikeshare/registry.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 032c6fe2a..98e0066f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +- Add Bike Share Toronto (1,069 stations) to the bikeshare registry. The system + publishes the same PBSC GBFS v2 feeds as the Austin, Honolulu and Chattanooga + entries, so it needs no new plumbing; a registry test now holds every city's + feed URLs against the proxy host and path allowlists, so a city the proxy + would refuse fails the suite instead of rendering an empty layer. + - GBFS v3 station names use English translations when available, then the first usable translation or short name. Empty or malformed names no longer appear as `[object Object]` or interrupt station loading; legacy string names remain diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index 040e53afc..3e1e0f114 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -58,6 +58,7 @@ How to read this: | **Open Calgary** (`data.calgary.ca`) | CCTV camera catalog + frames, Calgary | [Open Government Licence – City of Calgary](https://data.calgary.ca/stories/s/Open-Calgary-Terms-of-Use/u45n-7awa) — attribution REQUIRED | "Contains information licensed under the Open Government Licence – City of Calgary" | | **Statens vegvesen** (catalog `ogckart-sn1.atlas.vegvesen.no`, frames `kamera.atlas.vegvesen.no`, HLS `kamera.vegvesen.no`) | CCTV camera catalog + frames + live HLS video, Norway road cameras | [NLOD 2.0](https://data.norge.no/nlod/en/2.0) — attribution REQUIRED | "Contains data under the Norwegian licence for Open Government data (NLOD) distributed by Statens vegvesen" | | **GBFS (Lyft / BCycle)** | Bikeshare availability | Per-feed (attribution-only) | Credit the operator (e.g. Austin BCycle) + its `license_url` | +| **Bike Share Toronto** (`toronto.publicbikesystem.net`) | Bikeshare station availability, Toronto | Operator GBFS feed. The City's open-data package declares no licence and the feed publishes no `license_url`, so it is treated as attribution-only like the other GBFS operators. | "Bike Share Toronto (Toronto Parking Authority)" | | **MBTA / MassDOT** (cdn.mbta.com) | Transit layer live vehicles and up to 15 minutes of recently observed positions, Greater Boston | MBTA / MassDOT open realtime vehicle data, used for the live view and up to fifteen minutes of recently observed positions; published for developer use and not against their use. | Attribution to MBTA / MassDOT (courtesy text) | | **CapMetro** (data.texas.gov) | Transit layer live vehicles, Austin TX | CapMetro open realtime vehicle data, used for the live view; published for developer use and not against their use. | Attribution to CapMetro (courtesy text) | | **Metro Transit** (Metropolitan Council) | Transit layer live vehicles, Minneapolis–St Paul | Metro Transit / Metropolitan Council open realtime vehicle data, used for the live view; published for developer use and not against their use. | Attribution to Metro Transit / Metropolitan Council (courtesy text) | diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 0d3ef31e0..550d76d57 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3074,7 +3074,7 @@ its criteria cannot be silently ignored. | CCTV | Austin + Caltrans (CA) + TfL London + Ontario 511 + Fintraffic (FI) + DriveBC (BC) + TxDOT (TX) + Estonia (Tallinn, Tarktee) + Live Traffic NSW + Open Calgary Open Data + Statens vegvesen (NO, stills + live HLS) + Street View fallback | `src/data/cctv.js` | `/api/cctv` | 10s (active) | | Radio | Radio Browser (public-domain station directory) | `src/data/radio.js` | `/api/radio/stations`, `/api/radio/click/:uuid` | 45 min directory refresh | | Transit 🚌 | Operator GTFS-Realtime VehiclePositions (7 keyless regions, `src/data/transitFeeds.js`) | `src/layers/transit/` via `src/app/layers/transit.js` | `/api/transit` | 15s (poll + delayed playback) | -| Bikeshare 🚲 | GBFS (Lyft + BCycle) | `src/data/bikeshare.js` | `/api/gbfs` | 60s | +| Bikeshare 🚲 | GBFS (Lyft + BCycle + Bike Share Toronto) | `src/data/bikeshare.js` | `/api/gbfs` | 60s | | Directions 🧭 | OSRM on FOSSGIS servers (OpenStreetMap) | `src/data/directions.js` | `/api/route` (`steps=1`) | on placement / mode change | | Datacenters ▣ | OSM extract (bundled) | `src/data/localLayers.js` | — | static | | Dams ▰ | OpenInfraMap/OSM extract (bundled) | `src/data/localLayers.js` | — | static | diff --git a/src/data/dataCredits.js b/src/data/dataCredits.js index 2967f0a2b..2277abf4a 100644 --- a/src/data/dataCredits.js +++ b/src/data/dataCredits.js @@ -249,7 +249,9 @@ export const DATA_CREDITS = [ }, { key: 'gbfs', - html: 'Bikeshare availability: GBFS operator feeds (e.g. Austin BCycle)', + html: + 'Bikeshare availability: GBFS operator feeds ' + + '(e.g. Austin BCycle; Bike Share Toronto, operated by the Toronto Parking Authority)', }, { key: 'osrm-routing', diff --git a/src/layers/bikeshare/registry.js b/src/layers/bikeshare/registry.js index ff752f664..e9096b4a9 100644 --- a/src/layers/bikeshare/registry.js +++ b/src/layers/bikeshare/registry.js @@ -167,6 +167,18 @@ const RAW_GBFS_CITY_REGISTRY = [ 'https://chat.publicbikesystem.net/customer/gbfs/v2/en/station_status.json', provider: 'Bike Chattanooga', }, + { + id: 'toronto-bike-share', + city: 'Toronto, ON', + centerLat: 43.6532, + centerLon: -79.3832, + loadRadiusKm: 90, + stationInformationUrl: + 'https://toronto.publicbikesystem.net/customer/gbfs/v2/en/station_information.json', + stationStatusUrl: + 'https://toronto.publicbikesystem.net/customer/gbfs/v2/en/station_status.json', + provider: 'Bike Share Toronto', + }, bcycleEntry({ id: 'boulder-bcycle', city: 'Boulder, CO', diff --git a/src/layers/bikeshare/registry.test.mjs b/src/layers/bikeshare/registry.test.mjs new file mode 100644 index 000000000..30e708957 --- /dev/null +++ b/src/layers/bikeshare/registry.test.mjs @@ -0,0 +1,58 @@ +// BIKESHARE REGISTRY — a city entry is only useful if the proxy will serve it. +// +// A registry entry and the GBFS proxy allowlist are edited in different files, +// so a city can look correctly registered while every one of its requests is +// rejected at the proxy. These cases hold the two together: each entry is +// checked against the same host and path predicates the proxy enforces, so a +// new city that the proxy would refuse fails here rather than silently +// rendering an empty layer. +// +// Run with: npm test +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { GBFS_CITY_REGISTRY, CITY_BY_ID } from './registry.js'; +import { isAllowedGbfsHost, isAllowedGbfsPath } from '../../data/gbfsSource.js'; + +test('every registered city is reachable through the GBFS proxy allowlist', () => { + for (const city of GBFS_CITY_REGISTRY) { + for (const raw of [city.stationInformationUrl, city.stationStatusUrl]) { + const url = new URL(raw); + assert.equal( + isAllowedGbfsHost(url.hostname), + true, + `${city.id}: host ${url.hostname} is not allowlisted`, + ); + assert.equal( + isAllowedGbfsPath(url.pathname), + true, + `${city.id}: path ${url.pathname} is not allowlisted`, + ); + } + } +}); + +test('Bike Share Toronto is registered against the PBSC v2 feeds', () => { + const toronto = CITY_BY_ID.get('toronto-bike-share'); + assert.ok(toronto, 'toronto-bike-share is missing from the registry'); + assert.equal(toronto.city, 'Toronto, ON'); + assert.equal(toronto.provider, 'Bike Share Toronto'); + assert.equal( + toronto.stationInformationUrl, + 'https://toronto.publicbikesystem.net/customer/gbfs/v2/en/station_information.json', + ); + assert.equal( + toronto.stationStatusUrl, + 'https://toronto.publicbikesystem.net/customer/gbfs/v2/en/station_status.json', + ); +}); + +test('the Toronto center sits inside the system service area', () => { + const toronto = CITY_BY_ID.get('toronto-bike-share'); + // Bounding box of the 1,069 live stations, sampled from the published + // station_information feed: 43.588..43.813 N, -79.603..-79.123 W. + assert.ok(toronto.centerLat > 43.588 && toronto.centerLat < 43.813); + assert.ok(toronto.centerLon > -79.603 && toronto.centerLon < -79.123); + // The system spans roughly 37 km east-west, so the load radius has to clear + // half that from the center or downtown pans lose the outer stations. + assert.ok(toronto.loadRadiusKm >= 25); +});