Merge pull request #1447 from Rohilalala/fix/e-bikeshare-derived-capacity

fix(bikeshare): re-derive a station's missing capacity on every poll
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-11-02 07:06:39 -08:00
commit fb06eab03f
4 files changed
+67 -1

No files matched your search

@@ -0,0 +1,3 @@
- Bike Share stations whose feed publishes no capacity now take it from each
status poll's bikes and docks instead of keeping the first poll's value or
the placeholder 15 (Aditya, @Rohilalala, #1447).
+2
View File
@@ -1898,6 +1898,8 @@ limit, as well as when a tile decodes or the source resets.
GBFS null or empty availability counts retain an unknown value through station GBFS null or empty availability counts retain an unknown value through station
parsing and rendering; they use the neutral station color rather than reporting parsing and rendering; they use the neutral station color rather than reporting
an empty dock. Explicit zero and caller-supplied numeric fallbacks remain valid. an empty dock. Explicit zero and caller-supplied numeric fallbacks remain valid.
A station whose `station_information` publishes no capacity takes bikes plus
docks from each status poll (15 until its status arrives), not from the first.
## Terrain height cache bound ## Terrain height cache bound
+59
View File
@@ -446,3 +446,62 @@ test('unknown GBFS counts stay unknown through the source, model and station col
assert.equal(model.toNonNegativeInteger('3'), 3); assert.equal(model.toNonNegativeInteger('3'), 3);
} }
}); });
test('a station without a published capacity re-derives it from every status poll', async () => {
const { createRendering } = await import('./rendering.js');
const { COLOR_YELLOW } = await import('./policy.js');
const model = createModel({});
const state = {
_cityRuntime: new Map(),
_stationRenderMap: new Map(),
_pointCollection: { add: (options) => ({ ...options }) },
};
const rendering = createRendering({
state,
services: { render: { governorRequestRender() {} } },
parts: { model },
});
const info = model.parseStationInformation({
data: {
stations: [
{ station_id: 'open', name: 'Open', lat: 30.26, lon: -97.74 },
{
station_id: 'fixed',
name: 'Fixed',
lat: 30.27,
lon: -97.75,
capacity: 13,
},
],
},
});
rendering.ensureCityPoints('austin', info);
const poll = (stations) =>
rendering.applyStatusToPoints(
'austin',
model.parseStationStatus({ data: { stations } }),
);
const open = state._stationRenderMap.get('austin:open');
const fixed = state._stationRenderMap.get('austin:fixed');
// The first poll has no row for the station: capacity is a placeholder.
poll([
{
station_id: 'fixed',
num_bikes_available: 4,
num_docks_available: 9,
},
]);
poll([
{ station_id: 'open', num_bikes_available: 10, num_docks_available: 20 },
{ station_id: 'fixed', num_bikes_available: 2, num_docks_available: 3 },
]);
assert.equal(open.capacity, 30, 'the first poll fixed the capacity');
assert.equal(open.point.color, COLOR_YELLOW, '10 of 30 is not plenty');
assert.equal(fixed.capacity, 13, 'a published capacity is kept');
poll([
{ station_id: 'open', num_bikes_available: 4, num_docks_available: 2 },
]);
assert.equal(open.capacity, 6);
});
+3 -1
View File
@@ -185,6 +185,7 @@ export function createRendering({
stationName: station.name, stationName: station.name,
point, point,
capacity: parts.model.toNonNegativeInteger(station.capacity), capacity: parts.model.toNonNegativeInteger(station.capacity),
publishedCapacity: parts.model.toNonNegativeInteger(station.capacity),
bikesAvailable: null, bikesAvailable: null,
docksAvailable: null, docksAvailable: null,
isInstalled: station.isInstalled, isInstalled: station.isInstalled,
@@ -245,7 +246,8 @@ export function createRendering({
if (!record) continue; if (!record) continue;
const status = statusMap.get(record.stationId) || null; const status = statusMap.get(record.stationId) || null;
const capacity = resolveCapacity(record.capacity, status); // Resolve from the published capacity, not last poll's derived value.
const capacity = resolveCapacity(record.publishedCapacity, status);
record.capacity = capacity; record.capacity = capacity;
record.bikesAvailable = parts.model.toNonNegativeInteger( record.bikesAvailable = parts.model.toNonNegativeInteger(