mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
refactor: move GBFS station parsing into a portable source module
The bike-share layer parsed station documents inside its model factory. Move the parsers unchanged to src/sources/gbfsStations.js and have the model import them, so code outside the layer can read GBFS stations without constructing the layer.
This commit is contained in:
1 parent
e680a59bf5
commit
eb12303f46
3 files changed
+126
-119
No files matched your search
@@ -1080,6 +1080,7 @@
|
|||||||
"src/layers/bikeshare/ingestion.js",
|
"src/layers/bikeshare/ingestion.js",
|
||||||
"src/layers/bikeshare/lifecycle.js",
|
"src/layers/bikeshare/lifecycle.js",
|
||||||
"src/layers/bikeshare/model.js",
|
"src/layers/bikeshare/model.js",
|
||||||
|
"src/sources/gbfsStations.js",
|
||||||
"src/layers/bikeshare/policy.js",
|
"src/layers/bikeshare/policy.js",
|
||||||
"src/layers/bikeshare/queries.js",
|
"src/layers/bikeshare/queries.js",
|
||||||
"src/layers/bikeshare/registry.js",
|
"src/layers/bikeshare/registry.js",
|
||||||
@@ -1526,6 +1527,7 @@
|
|||||||
"src/layers/bikeshare/ingestion.js",
|
"src/layers/bikeshare/ingestion.js",
|
||||||
"src/layers/bikeshare/lifecycle.js",
|
"src/layers/bikeshare/lifecycle.js",
|
||||||
"src/layers/bikeshare/model.js",
|
"src/layers/bikeshare/model.js",
|
||||||
|
"src/sources/gbfsStations.js",
|
||||||
"src/layers/bikeshare/policy.js",
|
"src/layers/bikeshare/policy.js",
|
||||||
"src/layers/bikeshare/queries.js",
|
"src/layers/bikeshare/queries.js",
|
||||||
"src/layers/bikeshare/registry.js",
|
"src/layers/bikeshare/registry.js",
|
||||||
@@ -2191,6 +2193,7 @@
|
|||||||
"src/layers/bikeshare/ingestion.js",
|
"src/layers/bikeshare/ingestion.js",
|
||||||
"src/layers/bikeshare/lifecycle.js",
|
"src/layers/bikeshare/lifecycle.js",
|
||||||
"src/layers/bikeshare/model.js",
|
"src/layers/bikeshare/model.js",
|
||||||
|
"src/sources/gbfsStations.js",
|
||||||
"src/layers/bikeshare/policy.js",
|
"src/layers/bikeshare/policy.js",
|
||||||
"src/layers/bikeshare/queries.js",
|
"src/layers/bikeshare/queries.js",
|
||||||
"src/layers/bikeshare/registry.js",
|
"src/layers/bikeshare/registry.js",
|
||||||
|
|||||||
@@ -1,36 +1,12 @@
|
|||||||
|
import {
|
||||||
|
extractStationsArray,
|
||||||
|
normalizeGbfsBool,
|
||||||
|
parseStationInformation,
|
||||||
|
parseStationStatus,
|
||||||
|
toNonNegativeInteger,
|
||||||
|
} from '../../sources/gbfsStations.js';
|
||||||
|
|
||||||
export function createModel({ state: layerState, services, parts, source }) {
|
export function createModel({ state: layerState, services, parts, source }) {
|
||||||
/**
|
|
||||||
* Coerce a GBFS boolean field to a native boolean.
|
|
||||||
* GBFS feeds are inconsistent — some use booleans, others use 0/1 or strings.
|
|
||||||
* @param {*} value - Raw field value from GBFS JSON.
|
|
||||||
* @param {boolean} [fallback=true] - Default when value is null/undefined/unrecognized.
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function normalizeGbfsBool(value, fallback = true) {
|
|
||||||
if (value == null) return fallback;
|
|
||||||
if (typeof value === 'boolean') return value;
|
|
||||||
const num = Number(value);
|
|
||||||
if (Number.isFinite(num)) return num !== 0;
|
|
||||||
const text = String(value).trim().toLowerCase();
|
|
||||||
if (text === 'true' || text === 'yes') return true;
|
|
||||||
if (text === 'false' || text === 'no') return false;
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a value as a non-negative integer, returning fallback on failure.
|
|
||||||
* @param {*} value - Raw numeric value.
|
|
||||||
* @param {number|null} [fallback=null] - Returned when value is not a valid non-negative number.
|
|
||||||
* @returns {number|null}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function toNonNegativeInteger(value, fallback = null) {
|
|
||||||
const n = Number(value);
|
|
||||||
if (!Number.isFinite(n) || n < 0) return fallback;
|
|
||||||
return Math.round(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a globally unique render key for a station.
|
* Build a globally unique render key for a station.
|
||||||
* @param {string} cityId - City identifier.
|
* @param {string} cityId - City identifier.
|
||||||
@@ -63,93 +39,6 @@ export function createModel({ state: layerState, services, parts, source }) {
|
|||||||
return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract the stations array from a GBFS JSON payload.
|
|
||||||
* Handles multiple response shapes: { data: { stations: [...] } },
|
|
||||||
* { data: [...] }, and nested objects with a stations sub-key.
|
|
||||||
* @param {Object} payload - Parsed GBFS JSON response.
|
|
||||||
* @returns {Object[]} Array of raw station objects (may be empty).
|
|
||||||
*/
|
|
||||||
|
|
||||||
function extractStationsArray(payload) {
|
|
||||||
const data = payload?.data;
|
|
||||||
if (Array.isArray(data?.stations)) return data.stations;
|
|
||||||
if (Array.isArray(data)) return data;
|
|
||||||
// Some feeds nest stations under an additional key (e.g. locale wrappers)
|
|
||||||
if (data && typeof data === 'object') {
|
|
||||||
for (const value of Object.values(data)) {
|
|
||||||
if (Array.isArray(value?.stations)) return value.stations;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a GBFS station_information payload into a Map of station metadata.
|
|
||||||
* Handles field-name variations across different GBFS providers
|
|
||||||
* (station_id vs id, lat vs latitude, etc.). Stations with missing or
|
|
||||||
* invalid coordinates are silently skipped.
|
|
||||||
* @param {Object} payload - Parsed station_information.json response.
|
|
||||||
* @returns {Map<string, Object>} Map of stationId to station info objects.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function parseStationInformation(payload) {
|
|
||||||
const stations = extractStationsArray(payload);
|
|
||||||
const stationMap = new Map();
|
|
||||||
|
|
||||||
for (const raw of stations) {
|
|
||||||
// Accept both GBFS 2.x (station_id) and alternate (id) field names
|
|
||||||
const stationId = String(raw?.station_id ?? raw?.id ?? '').trim();
|
|
||||||
const lat = Number(raw?.lat ?? raw?.latitude);
|
|
||||||
const lon = Number(raw?.lon ?? raw?.longitude);
|
|
||||||
if (!stationId || !Number.isFinite(lat) || !Number.isFinite(lon))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
stationMap.set(stationId, {
|
|
||||||
stationId,
|
|
||||||
name: String(raw?.name || raw?.short_name || '').trim(),
|
|
||||||
lat,
|
|
||||||
lon,
|
|
||||||
capacity: toNonNegativeInteger(raw?.capacity),
|
|
||||||
isInstalled: normalizeGbfsBool(raw?.is_installed, true),
|
|
||||||
isRenting: normalizeGbfsBool(raw?.is_renting, true),
|
|
||||||
isReturning: normalizeGbfsBool(raw?.is_returning, true),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return stationMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a GBFS station_status payload into a Map of real-time availability.
|
|
||||||
* @param {Object} payload - Parsed station_status.json response.
|
|
||||||
* @returns {Map<string, Object>} Map of stationId to status objects containing
|
|
||||||
* bikesAvailable, docksAvailable, and operational flags.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function parseStationStatus(payload) {
|
|
||||||
const stations = extractStationsArray(payload);
|
|
||||||
const statusMap = new Map();
|
|
||||||
|
|
||||||
for (const raw of stations) {
|
|
||||||
const stationId = String(raw?.station_id ?? raw?.id ?? '').trim();
|
|
||||||
if (!stationId) continue;
|
|
||||||
|
|
||||||
const bikes = toNonNegativeInteger(raw?.num_bikes_available);
|
|
||||||
const docks = toNonNegativeInteger(raw?.num_docks_available);
|
|
||||||
statusMap.set(stationId, {
|
|
||||||
stationId,
|
|
||||||
bikesAvailable: bikes,
|
|
||||||
docksAvailable: docks,
|
|
||||||
isInstalled: normalizeGbfsBool(raw?.is_installed, true),
|
|
||||||
isRenting: normalizeGbfsBool(raw?.is_renting, true),
|
|
||||||
isReturning: normalizeGbfsBool(raw?.is_returning, true),
|
|
||||||
lastReported: toNonNegativeInteger(raw?.last_reported),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return statusMap;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
normalizeGbfsBool,
|
normalizeGbfsBool,
|
||||||
toNonNegativeInteger,
|
toNonNegativeInteger,
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/** Parse GBFS station documents, tolerating field-name variations across providers. */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerce a GBFS boolean field to a native boolean.
|
||||||
|
* GBFS feeds are inconsistent — some use booleans, others use 0/1 or strings.
|
||||||
|
* @param {*} value - Raw field value from GBFS JSON.
|
||||||
|
* @param {boolean} [fallback=true] - Default when value is null/undefined/unrecognized.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function normalizeGbfsBool(value, fallback = true) {
|
||||||
|
if (value == null) return fallback;
|
||||||
|
if (typeof value === 'boolean') return value;
|
||||||
|
const num = Number(value);
|
||||||
|
if (Number.isFinite(num)) return num !== 0;
|
||||||
|
const text = String(value).trim().toLowerCase();
|
||||||
|
if (text === 'true' || text === 'yes') return true;
|
||||||
|
if (text === 'false' || text === 'no') return false;
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a value as a non-negative integer, returning fallback on failure.
|
||||||
|
* @param {*} value - Raw numeric value.
|
||||||
|
* @param {number|null} [fallback=null] - Returned when value is not a valid non-negative number.
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
export function toNonNegativeInteger(value, fallback = null) {
|
||||||
|
const n = Number(value);
|
||||||
|
if (!Number.isFinite(n) || n < 0) return fallback;
|
||||||
|
return Math.round(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the stations array from a GBFS JSON payload.
|
||||||
|
* Handles multiple response shapes: { data: { stations: [...] } },
|
||||||
|
* { data: [...] }, and nested objects with a stations sub-key.
|
||||||
|
* @param {Object} payload - Parsed GBFS JSON response.
|
||||||
|
* @returns {Object[]} Array of raw station objects (may be empty).
|
||||||
|
*/
|
||||||
|
export function extractStationsArray(payload) {
|
||||||
|
const data = payload?.data;
|
||||||
|
if (Array.isArray(data?.stations)) return data.stations;
|
||||||
|
if (Array.isArray(data)) return data;
|
||||||
|
// Some feeds nest stations under an additional key (e.g. locale wrappers)
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
for (const value of Object.values(data)) {
|
||||||
|
if (Array.isArray(value?.stations)) return value.stations;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a GBFS station_information payload into a Map of station metadata.
|
||||||
|
* Handles field-name variations across different GBFS providers
|
||||||
|
* (station_id vs id, lat vs latitude, etc.). Stations with missing or
|
||||||
|
* invalid coordinates are silently skipped.
|
||||||
|
* @param {Object} payload - Parsed station_information.json response.
|
||||||
|
* @returns {Map<string, Object>} Map of stationId to station info objects.
|
||||||
|
*/
|
||||||
|
export function parseStationInformation(payload) {
|
||||||
|
const stations = extractStationsArray(payload);
|
||||||
|
const stationMap = new Map();
|
||||||
|
|
||||||
|
for (const raw of stations) {
|
||||||
|
// Accept both GBFS 2.x (station_id) and alternate (id) field names
|
||||||
|
const stationId = String(raw?.station_id ?? raw?.id ?? '').trim();
|
||||||
|
const lat = Number(raw?.lat ?? raw?.latitude);
|
||||||
|
const lon = Number(raw?.lon ?? raw?.longitude);
|
||||||
|
if (!stationId || !Number.isFinite(lat) || !Number.isFinite(lon)) continue;
|
||||||
|
|
||||||
|
stationMap.set(stationId, {
|
||||||
|
stationId,
|
||||||
|
name: String(raw?.name || raw?.short_name || '').trim(),
|
||||||
|
lat,
|
||||||
|
lon,
|
||||||
|
capacity: toNonNegativeInteger(raw?.capacity),
|
||||||
|
isInstalled: normalizeGbfsBool(raw?.is_installed, true),
|
||||||
|
isRenting: normalizeGbfsBool(raw?.is_renting, true),
|
||||||
|
isReturning: normalizeGbfsBool(raw?.is_returning, true),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return stationMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a GBFS station_status payload into a Map of real-time availability.
|
||||||
|
* @param {Object} payload - Parsed station_status.json response.
|
||||||
|
* @returns {Map<string, Object>} Map of stationId to status objects containing
|
||||||
|
* bikesAvailable, docksAvailable, and operational flags.
|
||||||
|
*/
|
||||||
|
export function parseStationStatus(payload) {
|
||||||
|
const stations = extractStationsArray(payload);
|
||||||
|
const statusMap = new Map();
|
||||||
|
|
||||||
|
for (const raw of stations) {
|
||||||
|
const stationId = String(raw?.station_id ?? raw?.id ?? '').trim();
|
||||||
|
if (!stationId) continue;
|
||||||
|
|
||||||
|
const bikes = toNonNegativeInteger(raw?.num_bikes_available);
|
||||||
|
const docks = toNonNegativeInteger(raw?.num_docks_available);
|
||||||
|
statusMap.set(stationId, {
|
||||||
|
stationId,
|
||||||
|
bikesAvailable: bikes,
|
||||||
|
docksAvailable: docks,
|
||||||
|
isInstalled: normalizeGbfsBool(raw?.is_installed, true),
|
||||||
|
isRenting: normalizeGbfsBool(raw?.is_renting, true),
|
||||||
|
isReturning: normalizeGbfsBool(raw?.is_returning, true),
|
||||||
|
lastReported: toNonNegativeInteger(raw?.last_reported),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusMap;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user