mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Merge branch 'main' into feat/changelog-fragments
This commit is contained in:
7 files changed
+236
-3
No files matched your search
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
- Add Bogotá (Tembici, 250 stations) and Santiago de Chile (Bike Itaú, 200
|
||||
stations) to the GBFS bikeshare registry, the layer's first stations in
|
||||
Colombia and Chile. Both are PBSC feeds like the Brazilian Bike Itaú cities,
|
||||
so no proxy change is needed. Both feeds also list depot stations on other
|
||||
continents (São Paulo, Madrid) as in service; stations outside a city's load
|
||||
radius are now dropped instead of drawn as live docks.
|
||||
|
||||
- Add Barcelona's bike share to the GBFS registry: Bicing (543 stations, the
|
||||
city's PBSC system) and AMBici (237 e-bike stations across the metropolitan
|
||||
area, run by nextbike). Both are keyless GBFS 2.3 feeds listed in
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ How to read this:
|
||||
| **City of Toronto Open Data** (`opendata.toronto.ca`) | CCTV camera catalog + frames, Toronto city streets | [Open Government Licence – Toronto](https://www.toronto.ca/city-government/data-research-maps/open-data/open-data-licence/) — attribution REQUIRED | "Contains information licensed under the Open Government Licence – Toronto" |
|
||||
| **GBFS (Lyft / BCycle / Social Bicycles / PBSC)** | Bikeshare availability | Per-feed (attribution-only) | Credit the operator (e.g. Austin BCycle) + its `license_url` |
|
||||
| **Bike Share Toronto** (`toronto.publicbikesystem.net`) | Bikeshare station availability, Toronto | Operator GBFS feed. No licence is published: the City's open-data package declares none and the feed has no `license_url`. Attribution is given to the operator. | "Bike Share Toronto (Toronto Parking Authority)" |
|
||||
| **Bike Itaú (Tembici)** (`*.publicbikesystem.net`, seven Brazilian cities) | Bikeshare station availability: São Paulo, Rio de Janeiro, Salvador, Recife, Porto Alegre, Brasília and Curitiba | Operator GBFS feeds. No licence is published: the feeds' `system_information.json` carries no `license_url`, operator or attribution fields. Attribution is given to the operator. | "Bike Itaú (Tembici)" |
|
||||
| **Bike Itaú / Tembici** (`*.publicbikesystem.net`, seven Brazilian cities, Santiago and Bogotá) | Bikeshare station availability: São Paulo, Rio de Janeiro, Salvador, Recife, Porto Alegre, Brasília, Curitiba, Santiago de Chile and Bogotá | Operator GBFS feeds. No licence is published: the feeds' `system_information.json` carries no `license_url`, operator or attribution fields. Attribution is given to the operator. | "Bike Itaú (Tembici)"; "Tembici Bogotá" |
|
||||
| **Bicing** (`barcelona.publicbikesystem.net`) | Bikeshare station availability, Barcelona | Operator GBFS feed (PBSC), keyless. The feed publishes no licence; the City's own open-data copy of the same station data is CC BY 4.0 but needs a token. Attribution is given to the operator. | "Bicing, Barcelona City Council (B:SM)" |
|
||||
| **AMBici** (nextbike, `gbfs.nextbike.net`) | Bikeshare station availability, Barcelona metropolitan area (e-bikes) | Operator GBFS feed, keyless. `system_information.license_id` is **CC0-1.0**; no attribution required, given anyway. | "AMBici, Àrea Metropolitana de Barcelona, operated by nextbike" |
|
||||
| **Oslo / Bergen / Trondheim Bysykkel** (Urban Sharing, `gbfs.urbansharing.com`) | Bikeshare availability, Oslo, Bergen and Trondheim city bikes. Keyless GBFS; the proxy sends the `Client-Identifier` header the operators ask for | [NLOD 2.0](https://data.norge.no/nlod/en/2.0) — attribution REQUIRED ([Oslo](https://oslobysykkel.no/en/open-data/realtime), [Bergen](https://bergenbysykkel.no/en/apne-data/sanntid), [Trondheim](https://trondheimbysykkel.no/en/open-data/realtime)) | "Contains data under the Norwegian licence for Open Government data (NLOD) made available by Oslo Bysykkel, Bergen Bysykkel and Trondheim Bysykkel" |
|
||||
|
||||
@@ -304,7 +304,7 @@ 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ú and Tembici Bogotá, operated by Tembici; ' +
|
||||
'Bicing, Barcelona City Council (B:SM); AMBici, Àrea Metropolitana de Barcelona, operated by nextbike)',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -97,7 +97,10 @@ export function createIngestion({
|
||||
const payload = await fetchGbfsJson(city.stationInformationUrl, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
const stationMap = parts.model.parseStationInformation(payload);
|
||||
const stationMap = parts.model.stationsWithinCity(
|
||||
city,
|
||||
parts.model.parseStationInformation(payload),
|
||||
);
|
||||
if (stationMap.size === 0) {
|
||||
throw new Error(`No station information for ${city.city}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { CITY_BY_ID } from './registry.js';
|
||||
import { createIngestion } from './ingestion.js';
|
||||
import { createModel } from './model.js';
|
||||
import { createViewport } from './viewport.js';
|
||||
import { isAllowedGbfsHost, isAllowedGbfsPath } from '../../data/gbfsSource.js';
|
||||
|
||||
// Each system's PBSC host, the station farthest from its registered centre,
|
||||
// and the depots its live feed lists on other continents (checked 2026-10-16).
|
||||
const CITIES = {
|
||||
'bogota-tembici': {
|
||||
host: 'bogota.publicbikesystem.net',
|
||||
provider: 'Tembici Bogotá',
|
||||
stations: [
|
||||
{ name: '067 - KR 7B Bis con CL 152A', lat: 4.727729, lon: -74.026271 },
|
||||
],
|
||||
depots: [
|
||||
{
|
||||
station_id: '316',
|
||||
name: 'Calle 13 - 316',
|
||||
lat: -23.4008649,
|
||||
lon: -46.3972181,
|
||||
},
|
||||
],
|
||||
},
|
||||
'santiago-bike-itau': {
|
||||
host: 'santiago.publicbikesystem.net',
|
||||
provider: 'Bike Itaú Santiago',
|
||||
stations: [
|
||||
{ name: 'LC077 - Mall Sport', lat: -33.3707813, lon: -70.5073122 },
|
||||
],
|
||||
depots: [
|
||||
{
|
||||
station_id: '459',
|
||||
name: 'Galpon - 459',
|
||||
lat: 40.4167747,
|
||||
lon: -3.7037899,
|
||||
},
|
||||
{
|
||||
station_id: '440',
|
||||
name: 'Galpon - 440',
|
||||
lat: -23.5690975,
|
||||
lon: -46.697366,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// 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('Bogotá and Santiago are registered and readable through the proxy', () => {
|
||||
for (const [id, expected] of Object.entries(CITIES)) {
|
||||
const city = CITY_BY_ID.get(id);
|
||||
assert.ok(city, `${id} is registered`);
|
||||
assert.equal(city.provider, expected.provider);
|
||||
assert.deepEqual(city.hosts, [expected.host]);
|
||||
for (const feed of [city.stationInformationUrl, city.stationStatusUrl]) {
|
||||
const url = new URL(feed);
|
||||
assert.ok(isAllowedGbfsHost(url.hostname), `${id} host ${url.hostname}`);
|
||||
assert.ok(isAllowedGbfsPath(url.pathname), `${id} path ${url.pathname}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('a camera over each outermost station activates its system', () => {
|
||||
for (const [id, expected] of Object.entries(CITIES)) {
|
||||
for (const station of expected.stations) {
|
||||
assert.ok(
|
||||
computeInRangeCities({ lat: station.lat, lon: station.lon }).has(id),
|
||||
`${station.name} activates ${id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('each load radius reaches its outermost stations', () => {
|
||||
// The range check floors every radius at 100 km; the station filter and
|
||||
// get_bike_share use the radius as registered.
|
||||
for (const [id, expected] of Object.entries(CITIES)) {
|
||||
const city = CITY_BY_ID.get(id);
|
||||
for (const station of expected.stations) {
|
||||
const distance = parts.model.haversineKm(
|
||||
city.centerLat,
|
||||
city.centerLon,
|
||||
station.lat,
|
||||
station.lon,
|
||||
);
|
||||
assert.ok(
|
||||
distance <= city.loadRadiusKm,
|
||||
`${station.name} is ${distance.toFixed(1)} km from the ${id} centre`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('stations a feed lists on another continent are not loaded', async () => {
|
||||
for (const [id, expected] of Object.entries(CITIES)) {
|
||||
const city = CITY_BY_ID.get(id);
|
||||
const outermost = expected.stations[0];
|
||||
const payload = {
|
||||
data: {
|
||||
stations: [
|
||||
{
|
||||
station_id: 'local',
|
||||
name: outermost.name,
|
||||
lat: outermost.lat,
|
||||
lon: outermost.lon,
|
||||
},
|
||||
...expected.depots,
|
||||
],
|
||||
},
|
||||
};
|
||||
const state = {
|
||||
_stationInfoCache: new Map(),
|
||||
_inFlightInfo: new Map(),
|
||||
_inFlightStatus: new Map(),
|
||||
_loadingOps: 0,
|
||||
_loading: false,
|
||||
};
|
||||
const ingestion = createIngestion({
|
||||
state,
|
||||
services: {},
|
||||
parts,
|
||||
source: {
|
||||
async getStations(url) {
|
||||
assert.equal(url, city.stationInformationUrl);
|
||||
return payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const stations = await ingestion.loadCityStationInfo(id, 1);
|
||||
assert.deepEqual(
|
||||
[...stations.keys()],
|
||||
['local'],
|
||||
`${id} keeps only local stations`,
|
||||
);
|
||||
assert.equal(state._stationInfoCache.get(id), stations);
|
||||
}
|
||||
});
|
||||
|
||||
test('a feed with only far-away stations reports no station information', async () => {
|
||||
const city = CITY_BY_ID.get('santiago-bike-itau');
|
||||
const ingestion = createIngestion({
|
||||
state: {
|
||||
_stationInfoCache: new Map(),
|
||||
_inFlightInfo: new Map(),
|
||||
_inFlightStatus: new Map(),
|
||||
_loadingOps: 0,
|
||||
_loading: false,
|
||||
},
|
||||
services: {},
|
||||
parts,
|
||||
source: {
|
||||
async getStations() {
|
||||
return { data: { stations: CITIES['santiago-bike-itau'].depots } };
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
ingestion.loadCityStationInfo('santiago-bike-itau', 1),
|
||||
new RegExp(`No station information for ${city.city}`),
|
||||
);
|
||||
});
|
||||
@@ -39,11 +39,36 @@ export function createModel({ state: layerState, services, parts, source }) {
|
||||
return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop stations outside a city's load radius. Some operators list depots on
|
||||
* other continents as in-service stations in a city's feed (Bogotá's and
|
||||
* Santiago's PBSC feeds carry warehouses in São Paulo and Madrid), which
|
||||
* would otherwise draw as live docks thousands of kilometres away.
|
||||
* @param {{ centerLat: number, centerLon: number, loadRadiusKm: number }} city
|
||||
* @param {Map<string, Object>} stationMap - Parsed station info map.
|
||||
* @returns {Map<string, Object>} Stations within the city's load radius.
|
||||
*/
|
||||
|
||||
function stationsWithinCity(city, stationMap) {
|
||||
const kept = new Map();
|
||||
for (const [stationId, station] of stationMap) {
|
||||
const distance = haversineKm(
|
||||
city.centerLat,
|
||||
city.centerLon,
|
||||
station.lat,
|
||||
station.lon,
|
||||
);
|
||||
if (distance <= city.loadRadiusKm) kept.set(stationId, station);
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
return {
|
||||
normalizeGbfsBool,
|
||||
toNonNegativeInteger,
|
||||
stationKey,
|
||||
haversineKm,
|
||||
stationsWithinCity,
|
||||
extractStationsArray,
|
||||
parseStationInformation,
|
||||
parseStationStatus,
|
||||
|
||||
@@ -330,6 +330,30 @@ const RAW_GBFS_CITY_REGISTRY = [
|
||||
'https://curitiba.publicbikesystem.net/customer/gbfs/v2/en/station_status.json',
|
||||
provider: 'Bike Itaú',
|
||||
},
|
||||
{
|
||||
id: 'bogota-tembici',
|
||||
city: 'Bogotá, Colombia',
|
||||
centerLat: 4.6533,
|
||||
centerLon: -74.0836,
|
||||
loadRadiusKm: 90,
|
||||
stationInformationUrl:
|
||||
'https://bogota.publicbikesystem.net/customer/gbfs/v2/en/station_information.json',
|
||||
stationStatusUrl:
|
||||
'https://bogota.publicbikesystem.net/customer/gbfs/v2/en/station_status.json',
|
||||
provider: 'Tembici Bogotá',
|
||||
},
|
||||
{
|
||||
id: 'santiago-bike-itau',
|
||||
city: 'Santiago, Chile',
|
||||
centerLat: -33.4372,
|
||||
centerLon: -70.6506,
|
||||
loadRadiusKm: 90,
|
||||
stationInformationUrl:
|
||||
'https://santiago.publicbikesystem.net/customer/gbfs/v2/en/station_information.json',
|
||||
stationStatusUrl:
|
||||
'https://santiago.publicbikesystem.net/customer/gbfs/v2/en/station_status.json',
|
||||
provider: 'Bike Itaú Santiago',
|
||||
},
|
||||
{
|
||||
id: 'toronto-bike-share',
|
||||
city: 'Toronto, ON',
|
||||
|
||||
Reference in new issue
Block a user