fix(bikeshare): re-derive a station's missing capacity on every poll

A station whose feed publishes no capacity froze its capacity at the
first status poll's bikes + docks, or at the placeholder 15 when that
poll had no row for it, so later cards read "10 avail · 20 docks · 15
cap" and coloured by the wrong ratio. Each poll now resolves it afresh.
This commit is contained in:
Aditya committed 2026-10-28 15:10:57 +05:30
1 parent a4d3701c3c
commit e244703f08
3 files changed
+64 -1

No files matched your search

+2
View File
@@ -1779,6 +1779,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(