mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(bikeshare): add Barcelona's Bicing and AMBici
- Bicing (543 stations, PBSC) and AMBici (237 e-bike stations across the metropolitan area, nextbike) join the GBFS registry from the operators' keyless GBFS 2.3 feeds, both listed in MobilityData's systems catalogue. - gbfs.nextbike.net joins the proxy allowlist as an exact host. - Credits, DATA_SOURCES, CURRENT-STATE and CHANGELOG updated; a test pins each system's proxy reachability and the range check for its outermost stations. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e2274bbf0c
commit
194595e1b6
7 files changed
+122
-2
No files matched your search
@@ -304,7 +304,8 @@ export const DATA_CREDITS = [
|
||||
key: 'gbfs',
|
||||
html:
|
||||
'Bikeshare availability: GBFS operator feeds ' +
|
||||
'(e.g. Austin BCycle; Bike Share Toronto, operated by the Toronto Parking Authority; Bike Itaú, operated by Tembici)',
|
||||
'(e.g. Austin BCycle; Bike Share Toronto, operated by the Toronto Parking Authority; Bike Itaú, operated by Tembici; ' +
|
||||
'Bicing, Barcelona City Council (B:SM); AMBici, Àrea Metropolitana de Barcelona, operated by nextbike)',
|
||||
},
|
||||
{
|
||||
key: 'urbansharing-gbfs',
|
||||
|
||||
@@ -8,6 +8,7 @@ const GBFS_ALLOWED_HOSTS = new Set([
|
||||
'austin.publicbikesystem.net',
|
||||
'hon.publicbikesystem.net',
|
||||
'chat.publicbikesystem.net',
|
||||
'gbfs.nextbike.net',
|
||||
'gbfs.urbansharing.com',
|
||||
'api.entur.io',
|
||||
'api.cyclocity.fr',
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { CITY_BY_ID } from './registry.js';
|
||||
import { createModel } from './model.js';
|
||||
import { createViewport } from './viewport.js';
|
||||
import { isAllowedGbfsHost, isAllowedGbfsPath } from '../../data/gbfsSource.js';
|
||||
|
||||
// Each Barcelona system's host, and the stations farthest from its registered
|
||||
// centre in the live feed (checked 2026-10-16). AMBici spans the metropolitan
|
||||
// area, so it lists its south-western and north-eastern ends.
|
||||
const SYSTEMS = {
|
||||
'barcelona-bicing': {
|
||||
host: 'barcelona.publicbikesystem.net',
|
||||
provider: 'Bicing',
|
||||
stations: [
|
||||
{ name: 'AV. RASOS DE PEGUERA, 10', lat: 41.462095, lon: 2.178959 },
|
||||
{ name: 'C/ 60, NÚMERO 25', lat: 41.3467746, lon: 2.1436235 },
|
||||
],
|
||||
},
|
||||
'barcelona-ambici': {
|
||||
host: 'gbfs.nextbike.net',
|
||||
provider: 'AMBici',
|
||||
stations: [
|
||||
{ name: 'Poal', lat: 41.268865, lon: 1.943849 },
|
||||
{ name: 'Platja del Manresà', lat: 41.460101, lon: 2.267271 },
|
||||
{ name: 'Can Ruti', lat: 41.482205, lon: 2.237607 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// The production range check, with no viewer or fetches behind it.
|
||||
const parts = {
|
||||
model: createModel({ state: {}, services: {}, parts: {}, source: null }),
|
||||
};
|
||||
const { computeInRangeCities } = createViewport({
|
||||
state: {},
|
||||
services: { render: { governorRequestRender() {} } },
|
||||
parts,
|
||||
source: null,
|
||||
});
|
||||
|
||||
test('Bicing and AMBici are registered and readable through the proxy', () => {
|
||||
for (const [id, expected] of Object.entries(SYSTEMS)) {
|
||||
const system = CITY_BY_ID.get(id);
|
||||
assert.ok(system, `${id} is registered`);
|
||||
assert.equal(system.provider, expected.provider);
|
||||
assert.deepEqual(system.hosts, [expected.host]);
|
||||
for (const feed of [
|
||||
system.stationInformationUrl,
|
||||
system.stationStatusUrl,
|
||||
]) {
|
||||
const url = new URL(feed);
|
||||
assert.ok(isAllowedGbfsHost(url.hostname), `${id} host ${url.hostname}`);
|
||||
assert.ok(
|
||||
isAllowedGbfsPath(url.pathname, url.hostname),
|
||||
`${id} path ${url.pathname}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('a camera over each outermost station activates its system', () => {
|
||||
for (const [id, expected] of Object.entries(SYSTEMS)) {
|
||||
for (const station of expected.stations) {
|
||||
assert.ok(
|
||||
computeInRangeCities({ lat: station.lat, lon: station.lon }).has(id),
|
||||
`${station.name} activates ${id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('a camera over central Barcelona activates both systems', () => {
|
||||
// Plaça de Catalunya.
|
||||
const active = computeInRangeCities({ lat: 41.387, lon: 2.17 });
|
||||
assert.ok(active.has('barcelona-bicing'));
|
||||
assert.ok(active.has('barcelona-ambici'));
|
||||
});
|
||||
|
||||
test('the nextbike allowance is that one host, not its domain', () => {
|
||||
assert.equal(isAllowedGbfsHost('gbfs.nextbike.net'), true);
|
||||
assert.equal(isAllowedGbfsHost('api.nextbike.net'), false);
|
||||
assert.equal(isAllowedGbfsHost('gbfs.nextbike.net.example'), false);
|
||||
});
|
||||
@@ -554,6 +554,32 @@ const RAW_GBFS_CITY_REGISTRY = [
|
||||
'https://api.cyclocity.fr/contracts/lillestrom/gbfs/station_status.json',
|
||||
provider: 'Bysykkel Lillestrøm',
|
||||
},
|
||||
{
|
||||
id: 'barcelona-bicing',
|
||||
city: 'Barcelona, Spain',
|
||||
centerLat: 41.3926,
|
||||
centerLon: 2.164,
|
||||
loadRadiusKm: 40,
|
||||
stationInformationUrl:
|
||||
'https://barcelona.publicbikesystem.net/customer/gbfs/v2/en/station_information.json',
|
||||
stationStatusUrl:
|
||||
'https://barcelona.publicbikesystem.net/customer/gbfs/v2/en/station_status.json',
|
||||
provider: 'Bicing',
|
||||
},
|
||||
// AMBici covers the metropolitan municipalities around Barcelona, from
|
||||
// Castelldefels to Badalona, and overlaps Bicing only at the city edge.
|
||||
{
|
||||
id: 'barcelona-ambici',
|
||||
city: 'Barcelona metropolitan area, Spain',
|
||||
centerLat: 41.37,
|
||||
centerLon: 2.07,
|
||||
loadRadiusKm: 40,
|
||||
stationInformationUrl:
|
||||
'https://gbfs.nextbike.net/maps/gbfs/v2/nextbike_bs/en/station_information.json',
|
||||
stationStatusUrl:
|
||||
'https://gbfs.nextbike.net/maps/gbfs/v2/nextbike_bs/en/station_status.json',
|
||||
provider: 'AMBici',
|
||||
},
|
||||
];
|
||||
function normalizeRegistryEntry(entry) {
|
||||
const id = String(entry?.id || '')
|
||||
|
||||
Reference in new issue
Block a user