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

+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);
});