diff --git a/src/sources/gbfsStations.js b/src/sources/gbfsStations.js index dd4eff898..1bfeb037a 100644 --- a/src/sources/gbfsStations.js +++ b/src/sources/gbfsStations.js @@ -81,6 +81,21 @@ function stationName(value) { return fallback; } +/** + * Docks a station physically has. PBSC adds the bikes a station accepts in its + * geofenced area around the docks to `capacity` and reports that part as + * `geofenced_capacity` (not a GBFS field): Bilbao Bizi publishes 39 docks as + * capacity 839 with geofenced_capacity 800. Without the extension, or when it + * would exceed the total, `capacity` is read as given. + */ +function dockCapacity(raw) { + const capacity = toNonNegativeInteger(raw?.capacity); + const geofenced = toNonNegativeInteger(raw?.geofenced_capacity); + if (capacity == null || geofenced == null || geofenced > capacity) + return capacity; + return capacity - geofenced; +} + /** * Parse a GBFS station_information payload into a Map of station metadata. * Handles field-name variations across different GBFS providers @@ -105,7 +120,7 @@ export function parseStationInformation(payload) { name: stationName(raw?.name) || stationName(raw?.short_name), lat, lon, - capacity: toNonNegativeInteger(raw?.capacity), + capacity: dockCapacity(raw), isInstalled: normalizeGbfsBool(raw?.is_installed, true), isRenting: normalizeGbfsBool(raw?.is_renting, true), isReturning: normalizeGbfsBool(raw?.is_returning, true), diff --git a/src/sources/gbfsStations.test.mjs b/src/sources/gbfsStations.test.mjs index 6f12db258..91983df98 100644 --- a/src/sources/gbfsStations.test.mjs +++ b/src/sources/gbfsStations.test.mjs @@ -15,6 +15,25 @@ const parseName = (fields, version) => data: { stations: [station(fields)] }, }).get('s1').name; +const parseCapacity = (fields) => + parseStationInformation({ data: { stations: [station(fields)] } }).get('s1') + .capacity; + +test('capacity counts docks, not the geofenced area PBSC adds to it', () => { + // Bilbao Bizi, BiciMAD and Dbizi as published on 2026-10-17. + assert.equal(parseCapacity({ capacity: 839, geofenced_capacity: 800 }), 39); + assert.equal(parseCapacity({ capacity: 43, geofenced_capacity: 20 }), 23); + assert.equal(parseCapacity({ capacity: 19, geofenced_capacity: 0 }), 19); +}); + +test('capacity is read as given without a usable geofenced_capacity', () => { + assert.equal(parseCapacity({ capacity: 27 }), 27); + assert.equal(parseCapacity({ capacity: 27, geofenced_capacity: null }), 27); + assert.equal(parseCapacity({ capacity: 27, geofenced_capacity: 'n/a' }), 27); + assert.equal(parseCapacity({ capacity: 10, geofenced_capacity: 30 }), 10); + assert.equal(parseCapacity({ capacity: null, geofenced_capacity: 5 }), null); +}); + test('GBFS v2 station names remain trimmed strings', () => { assert.equal( parseName({ name: ' Republic Square ', short_name: 'Short' }, '2.3'),