mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge branch 'main' of https://github.com/bilawalsidhu/gods-eye-view into fix/tle-catalog-matching
This commit is contained in:
commit
a6fcfd38e3
91 files changed
+14743
-1404
No files matched your search
@@ -50,6 +50,26 @@ export function extractStationsArray(payload) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/** Read legacy text or a GBFS v3 Localized String, preferring usable English text. */
|
||||
function stationName(value) {
|
||||
if (typeof value === 'string') return value.trim();
|
||||
if (!Array.isArray(value)) return '';
|
||||
|
||||
let fallback = '';
|
||||
for (const translation of value) {
|
||||
if (typeof translation?.text !== 'string') continue;
|
||||
const text = translation.text.trim();
|
||||
if (!text) continue;
|
||||
if (!fallback) fallback = text;
|
||||
if (
|
||||
typeof translation.language === 'string' &&
|
||||
/^en(?:-|$)/i.test(translation.language.trim())
|
||||
)
|
||||
return text;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a GBFS station_information payload into a Map of station metadata.
|
||||
* Handles field-name variations across different GBFS providers
|
||||
@@ -63,7 +83,7 @@ export function parseStationInformation(payload) {
|
||||
const stationMap = new Map();
|
||||
|
||||
for (const raw of stations) {
|
||||
// Accept both GBFS 2.x (station_id) and alternate (id) field names
|
||||
// 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);
|
||||
@@ -71,7 +91,7 @@ export function parseStationInformation(payload) {
|
||||
|
||||
stationMap.set(stationId, {
|
||||
stationId,
|
||||
name: String(raw?.name || raw?.short_name || '').trim(),
|
||||
name: stationName(raw?.name) || stationName(raw?.short_name),
|
||||
lat,
|
||||
lon,
|
||||
capacity: toNonNegativeInteger(raw?.capacity),
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { parseStationInformation } from './gbfsStations.js';
|
||||
|
||||
const station = (fields = {}) => ({
|
||||
station_id: 's1',
|
||||
lat: 30.2681,
|
||||
lon: -97.7469,
|
||||
capacity: 13,
|
||||
...fields,
|
||||
});
|
||||
const parseName = (fields, version) =>
|
||||
parseStationInformation({
|
||||
version,
|
||||
data: { stations: [station(fields)] },
|
||||
}).get('s1').name;
|
||||
|
||||
test('GBFS v2 station names remain trimmed strings', () => {
|
||||
assert.equal(
|
||||
parseName({ name: ' Republic Square ', short_name: 'Short' }, '2.3'),
|
||||
'Republic Square',
|
||||
);
|
||||
});
|
||||
|
||||
test('GBFS v3 station names prefer English over the first translation', () => {
|
||||
assert.equal(
|
||||
parseName(
|
||||
{
|
||||
name: [
|
||||
{ language: 'hu', text: 'Köztársaság tér' },
|
||||
{ language: 'en', text: ' Republic Square ' },
|
||||
],
|
||||
},
|
||||
'3.0',
|
||||
),
|
||||
'Republic Square',
|
||||
);
|
||||
});
|
||||
|
||||
test('English preference accepts case differences and regional language tags', () => {
|
||||
for (const language of ['EN', 'en-GB', 'En-US']) {
|
||||
assert.equal(
|
||||
parseName({
|
||||
name: [
|
||||
{ language: 'hu', text: 'Hungarian' },
|
||||
{ language, text: 'English' },
|
||||
],
|
||||
}),
|
||||
'English',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('without usable English text the first usable translation wins', () => {
|
||||
assert.equal(
|
||||
parseName({
|
||||
name: [
|
||||
null,
|
||||
{ language: 'en', text: ' ' },
|
||||
{ language: 'en', text: 42 },
|
||||
{ language: 'hu', text: ' Első ' },
|
||||
{ language: 'fr', text: 'Deuxième' },
|
||||
],
|
||||
}),
|
||||
'Első',
|
||||
);
|
||||
});
|
||||
|
||||
test('malformed language fields cannot abort translation selection', () => {
|
||||
const name = [
|
||||
null,
|
||||
false,
|
||||
'bad',
|
||||
{},
|
||||
{ language: 42, text: 'Fallback' },
|
||||
{ language: { unexpected: true }, text: 'Other' },
|
||||
{ language: 'en', text: 'English' },
|
||||
];
|
||||
assert.equal(parseName({ name }), 'English');
|
||||
assert.equal(
|
||||
parseName({ name: [{ text: ' Usable text without a locale ' }] }),
|
||||
'Usable text without a locale',
|
||||
);
|
||||
});
|
||||
|
||||
test('a usable name takes precedence over an English short name', () => {
|
||||
assert.equal(
|
||||
parseName({
|
||||
name: [{ language: 'hu', text: 'Full name' }],
|
||||
short_name: [{ language: 'en', text: 'Short name' }],
|
||||
}),
|
||||
'Full name',
|
||||
);
|
||||
});
|
||||
|
||||
test('missing, empty and malformed names fall back to localized short_name', () => {
|
||||
for (const name of [
|
||||
undefined,
|
||||
null,
|
||||
'',
|
||||
' ',
|
||||
[],
|
||||
{},
|
||||
0,
|
||||
42,
|
||||
false,
|
||||
[null, {}, { language: 'en', text: [] }, { language: 'hu', text: ' ' }],
|
||||
]) {
|
||||
assert.equal(
|
||||
parseName({
|
||||
name,
|
||||
short_name: [
|
||||
{ language: 'hu', text: 'Rövid' },
|
||||
{ language: 'en', text: ' Short ' },
|
||||
],
|
||||
}),
|
||||
'Short',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('localized names can fall back to legacy short_name strings', () => {
|
||||
assert.equal(parseName({ name: [], short_name: ' Short ' }), 'Short');
|
||||
});
|
||||
|
||||
test('no usable name leaves the station available with an empty name', () => {
|
||||
for (const value of [
|
||||
undefined,
|
||||
null,
|
||||
'',
|
||||
[],
|
||||
{},
|
||||
42,
|
||||
true,
|
||||
[null, { text: false }, { language: 'en', text: ' ' }],
|
||||
]) {
|
||||
assert.equal(parseName({ name: value, short_name: value }), '');
|
||||
}
|
||||
});
|
||||
|
||||
test('name shape is normalized even when a feed omits or misstates its version', () => {
|
||||
const name = [{ language: 'en', text: 'Republic Square' }];
|
||||
for (const version of [undefined, '2.3', '3.0', '3.1', 'unknown']) {
|
||||
assert.equal(parseName({ name }, version), 'Republic Square');
|
||||
assert.equal(parseName({ name: 'Legacy name' }, version), 'Legacy name');
|
||||
}
|
||||
});
|
||||
|
||||
test('bad names do not lose valid stations or their metadata', () => {
|
||||
const information = parseStationInformation({
|
||||
version: '3.0',
|
||||
data: {
|
||||
stations: [
|
||||
station({ station_id: 'bad', name: [null, {}] }),
|
||||
station({
|
||||
station_id: 'short',
|
||||
name: [],
|
||||
short_name: [{ language: 'en', text: 'Short' }],
|
||||
}),
|
||||
station({
|
||||
station_id: 'good',
|
||||
name: [{ language: 'en', text: 'Republic Square' }],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.equal(information.size, 3);
|
||||
assert.equal(information.get('bad').name, '');
|
||||
assert.equal(information.get('short').name, 'Short');
|
||||
assert.deepEqual(information.get('good'), {
|
||||
stationId: 'good',
|
||||
name: 'Republic Square',
|
||||
lat: 30.2681,
|
||||
lon: -97.7469,
|
||||
capacity: 13,
|
||||
isInstalled: true,
|
||||
isRenting: true,
|
||||
isReturning: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('normalizing a localized name leaves the feed document unchanged', () => {
|
||||
const payload = {
|
||||
version: '3.0',
|
||||
data: {
|
||||
stations: [
|
||||
station({
|
||||
name: [
|
||||
{ language: 'hu', text: 'Magyar' },
|
||||
{ language: 'en', text: ' English ' },
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
const original = structuredClone(payload);
|
||||
parseStationInformation(payload);
|
||||
assert.deepEqual(payload, original);
|
||||
});
|
||||
Reference in new issue
Block a user