mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge meirm/i18n-spanish: latest upstream main
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
0ed05568f7
14 files changed
+575
-6
No files matched your search
@@ -0,0 +1,104 @@
|
||||
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('each load radius reaches its outermost stations', () => {
|
||||
// The range check floors every radius at 100 km, so the test above passes
|
||||
// with any loadRadiusKm; get_bike_share uses the radius as registered.
|
||||
for (const [id, expected] of Object.entries(SYSTEMS)) {
|
||||
const system = CITY_BY_ID.get(id);
|
||||
for (const station of expected.stations) {
|
||||
const distance = parts.model.haversineKm(
|
||||
system.centerLat,
|
||||
system.centerLon,
|
||||
station.lat,
|
||||
station.lon,
|
||||
);
|
||||
assert.ok(
|
||||
distance <= system.loadRadiusKm,
|
||||
`${station.name} is ${distance.toFixed(1)} km from the ${id} centre`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
@@ -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',
|
||||
@@ -554,6 +578,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