mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Merge current public main with PR #1232
This commit is contained in:
commit
a07b5a65fd
5 files changed
+132
-4
No files matched your search
@@ -0,0 +1,3 @@
|
||||
- Skip bike-share stations with missing, blank or invalid coordinates instead
|
||||
of placing them at coerced zero coordinates or accepting out-of-range values.
|
||||
Explicit zero coordinates and numeric strings remain supported (Bortlesboat, #1232).
|
||||
@@ -1531,7 +1531,11 @@ camera is refused there with a JSON 404 and is served solely through
|
||||
## GBFS upstream bounds
|
||||
|
||||
Station information is normalized in `src/sources/gbfsStations.js` for both the
|
||||
globe layer and `get_bike_share`. Names accept legacy strings or GBFS v3 Localized
|
||||
globe layer and `get_bike_share`. Stations with missing, blank, nonnumeric or
|
||||
out-of-range coordinates (latitude outside ±90, longitude outside ±180) are
|
||||
skipped. Numbers and nonblank numeric strings are accepted, including genuine
|
||||
zero and the `latitude`/`longitude` aliases; other JSON types are rejected.
|
||||
Names accept legacy strings or GBFS v3 Localized
|
||||
String arrays regardless of the reported feed version. Nonempty English text
|
||||
(including regional `en-*` tags, case-insensitive) takes precedence, then the first
|
||||
usable translation. An unusable `name` falls back to `short_name` with the same
|
||||
|
||||
@@ -98,6 +98,13 @@ function dockCapacity(raw) {
|
||||
return capacity - geofenced;
|
||||
}
|
||||
|
||||
/** Read a numeric coordinate without treating blank text or other JSON types as zero. */
|
||||
function coordinateNumber(value) {
|
||||
if (typeof value !== 'number' && (typeof value !== 'string' || !value.trim()))
|
||||
return NaN;
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a GBFS station_information payload into a Map of station metadata.
|
||||
* Handles field-name variations across different GBFS providers
|
||||
@@ -113,9 +120,16 @@ export function parseStationInformation(payload) {
|
||||
for (const raw of stations) {
|
||||
// Accept both GBFS (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;
|
||||
const lat = coordinateNumber(raw?.lat ?? raw?.latitude);
|
||||
const lon = coordinateNumber(raw?.lon ?? raw?.longitude);
|
||||
if (
|
||||
!stationId ||
|
||||
!Number.isFinite(lat) ||
|
||||
!Number.isFinite(lon) ||
|
||||
Math.abs(lat) > 90 ||
|
||||
Math.abs(lon) > 180
|
||||
)
|
||||
continue;
|
||||
|
||||
stationMap.set(stationId, {
|
||||
stationId,
|
||||
|
||||
@@ -19,6 +19,73 @@ const parseCapacity = (fields) =>
|
||||
parseStationInformation({ data: { stations: [station(fields)] } }).get('s1')
|
||||
.capacity;
|
||||
|
||||
test('station information skips missing, blank and malformed coordinates', () => {
|
||||
for (const field of ['lat', 'lon', 'latitude', 'longitude']) {
|
||||
for (const value of [
|
||||
undefined,
|
||||
null,
|
||||
'',
|
||||
' \t ',
|
||||
false,
|
||||
true,
|
||||
[],
|
||||
[0],
|
||||
{},
|
||||
'bad',
|
||||
NaN,
|
||||
Infinity,
|
||||
]) {
|
||||
const raw = { station_id: 'bad', lat: 0, lon: 0 };
|
||||
if (field === 'latitude') delete raw.lat;
|
||||
if (field === 'longitude') delete raw.lon;
|
||||
raw[field] = value;
|
||||
const parsed = parseStationInformation({
|
||||
data: { stations: [raw, station({ station_id: 'good' })] },
|
||||
});
|
||||
assert.deepEqual(
|
||||
[...parsed.keys()],
|
||||
['good'],
|
||||
`${field} = ${JSON.stringify(value)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('station information skips coordinates outside latitude and longitude bounds', () => {
|
||||
for (const fields of [
|
||||
{ lat: 90.001 },
|
||||
{ lat: -90.001 },
|
||||
{ lon: 180.001 },
|
||||
{ lon: -180.001 },
|
||||
{ lat: undefined, latitude: '91' },
|
||||
{ lon: undefined, longitude: '-181' },
|
||||
]) {
|
||||
const parsed = parseStationInformation({
|
||||
data: { stations: [station(fields)] },
|
||||
});
|
||||
assert.equal(parsed.size, 0, JSON.stringify(fields));
|
||||
}
|
||||
});
|
||||
|
||||
test('station information preserves zero, boundary coordinates and numeric aliases', () => {
|
||||
for (const [fields, expected] of [
|
||||
[{ lat: 0, lon: 0 }, [0, 0]],
|
||||
[{ lat: ' 0 ', lon: '0' }, [0, 0]],
|
||||
[{ lat: 90, lon: 180 }, [90, 180]],
|
||||
[{ lat: '-90', lon: '-180' }, [-90, -180]],
|
||||
[
|
||||
{ lat: null, lon: undefined, latitude: '30.2681', longitude: '-97.7469' },
|
||||
[30.2681, -97.7469],
|
||||
],
|
||||
]) {
|
||||
const parsed = parseStationInformation({
|
||||
data: { stations: [station(fields)] },
|
||||
}).get('s1');
|
||||
assert.ok(parsed, JSON.stringify(fields));
|
||||
assert.deepEqual([parsed.lat, parsed.lon], expected);
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -171,6 +171,46 @@ test('bike share reports uncovered areas and failed systems honestly', async ()
|
||||
);
|
||||
});
|
||||
|
||||
test('bike share excludes blank coordinates without losing stations on the prime meridian', async () => {
|
||||
const stations = [
|
||||
{ station_id: 'blank', lat: 51.5, lon: ' ' },
|
||||
{ station_id: 'zero', lat: 51.5, lon: 0 },
|
||||
];
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: {
|
||||
bikeshare: {
|
||||
systems: [system('fixture', 'Fixture Bikes', 51.5, 0)],
|
||||
async getStations(url) {
|
||||
return {
|
||||
data: {
|
||||
stations: url.endsWith('station_information.json')
|
||||
? stations
|
||||
: stations.map(({ station_id }) => ({
|
||||
station_id,
|
||||
num_bikes_available: 4,
|
||||
})),
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = await catalog.call('get_bike_share', {
|
||||
area: { lat: 51.5, lon: 0, radius_km: 2 },
|
||||
});
|
||||
assert.deepEqual(
|
||||
result.data.rows.map((row) => row.station_id),
|
||||
['zero'],
|
||||
);
|
||||
assert.equal(result.data.total, 1);
|
||||
assert.equal(result.data.rows[0].lon, 0);
|
||||
assert.equal(
|
||||
result.summary,
|
||||
'1 bike-share station in 2 km around 51.500, 0.000 with 4 bikes available.',
|
||||
);
|
||||
});
|
||||
|
||||
const feed = {
|
||||
id: 'capmetro',
|
||||
name: 'CapMetro',
|
||||
|
||||
Reference in new issue
Block a user