feat(bikeshare): add Bike Share Toronto

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.
This commit is contained in:
Cliff Flood committed 2026-10-13 18:55:20 -04:00
1 parent 132b9e96dc
commit b8ccfd1648
6 files changed
+81 -2

No files matched your search

+6
View File
@@ -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
+1
View File
@@ -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) |
+1 -1
View File
@@ -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 |
+3 -1
View File
@@ -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',
+12
View File
@@ -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',
+58
View File
@@ -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);
});