From 9bee985abbf83755cb32b0f2735e316558f0db68 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Tue, 13 Oct 2026 14:04:09 -0400 Subject: [PATCH] fix(bikeshare): preserve unknown availability counts --- CHANGELOG.md | 2 + docs/CURRENT-STATE.md | 3 ++ src/layers/bikeshare/proxyRoute.test.mjs | 52 ++++++++++++++++++++++++ src/sources/gbfsStations.js | 1 + 4 files changed, 58 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f6fb2f3a..68530b1bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ - Tool string length validation counts Unicode characters correctly, including characters outside the basic multilingual plane. - Director timeline seeks cross the dateline along the short longitude arc. +- Keep unknown GBFS availability counts neutral instead of turning null or empty + values into zero bikes. - Recent Imagery continues past malformed records in full catalog pages, respects the requested record limit, and reports results as truncated when that limit drops valid records. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 98a302c0f..1caf7f799 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1275,6 +1275,9 @@ and stale/error responses remain unchanged. Each has a Node-only package entry under `gods-eye-view/server/providers/`. Portable terrain mechanics, traffic tile math and GBFS source rules are available under `gods-eye-view/sources/`. The browser layers and their rendering remain in their existing modules. +GBFS null or empty availability counts retain an unknown value through station +parsing and rendering; they use the neutral station color rather than reporting +an empty dock. Explicit zero and caller-supplied numeric fallbacks remain valid. ## Terrain height cache bound diff --git a/src/layers/bikeshare/proxyRoute.test.mjs b/src/layers/bikeshare/proxyRoute.test.mjs index 9bb04ced0..e4153a203 100644 --- a/src/layers/bikeshare/proxyRoute.test.mjs +++ b/src/layers/bikeshare/proxyRoute.test.mjs @@ -394,3 +394,55 @@ for (const hook of ['configureServer', 'configurePreviewServer']) { ); }); } + +test('unknown GBFS counts stay unknown through the source, model and station color', async () => { + const { createRendering } = await import('./rendering.js'); + const { COLOR_NEUTRAL, COLOR_RED } = await import('./policy.js'); + for (const missing of [null, '']) { + const source = createBikeshareSource({ + fetchImpl: async () => + Response.json({ + data: { + stations: [ + { + station_id: '2537', + num_bikes_available: missing, + num_docks_available: 4, + is_installed: true, + is_renting: true, + is_returning: true, + }, + ], + }, + }), + }); + const model = createModel({}); + const status = model + .parseStationStatus(await source.getStations(AUSTIN.stationStatusUrl)) + .get('2537'); + const rendering = createRendering({ + state: {}, + services: { render: {} }, + parts: { model }, + }); + assert.equal(status.bikesAvailable, null); + assert.equal( + model.toNonNegativeInteger(status.bikesAvailable), + null, + 'render-map normalization preserves unknown', + ); + assert.equal(rendering.statusToColor(status, 12), COLOR_NEUTRAL); + assert.equal(rendering.resolveCapacity(missing, status), 4); + assert.equal( + rendering.statusToColor({ ...status, bikesAvailable: 0 }, 12), + COLOR_RED, + 'reported zero is still empty', + ); + assert.equal( + model.toNonNegativeInteger(missing, 7), + 7, + 'caller fallback is preserved', + ); + assert.equal(model.toNonNegativeInteger('3'), 3); + } +}); diff --git a/src/sources/gbfsStations.js b/src/sources/gbfsStations.js index efa1e6169..74f2efd23 100644 --- a/src/sources/gbfsStations.js +++ b/src/sources/gbfsStations.js @@ -25,6 +25,7 @@ export function normalizeGbfsBool(value, fallback = true) { * @returns {number|null} */ export function toNonNegativeInteger(value, fallback = null) { + if (value == null || value === '') return fallback; const n = Number(value); if (!Number.isFinite(n) || n < 0) return fallback; return Math.round(n);