From ace21600023665673101e087675eae7a62b6a670 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Wed, 11 Nov 2026 14:54:01 -0800 Subject: [PATCH] fix(rail): Montpellier tram speeds are converted from km/h TaM publishes km/h where GTFS-Realtime asks m/s, and the Transit layer's entry for the same feed already sets speedUnit: 'km/h'. The Rail entry did not, so the proxy passed the readings through and the card multiplied them by 3.6 again: a tram at 30 km/h read about 108 km/h. A registry test now requires a Rail feed that Transit also reads to declare the same unit. --- src/data/railFeeds.js | 9 ++++++++- src/data/railFeeds.test.mjs | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/data/railFeeds.js b/src/data/railFeeds.js index f1c1eec1d..6b7215c3c 100644 --- a/src/data/railFeeds.js +++ b/src/data/railFeeds.js @@ -476,7 +476,9 @@ const OWNER_AUTHORIZED_NO_TERMS = * 64 MiB: railMaxBodyBytes; the feeds that set one publish small documents * and set a lower cap), and `headerClock: 'untrusted'` marks a feed whose * header time is wrong and whose vehicles carry none: every train is then - * stamped with the fetch time. + * stamped with the fetch time. `speedUnit: 'km/h'` marks a feed that + * publishes km/h where the spec requires m/s; the proxy converts it, as for + * Transit (src/data/transitFeeds.js). * `tripUpdatesUrl` is the operator's GTFS-RT TripUpdates feed, served as * `/api/rail/updates/`. * @@ -509,6 +511,7 @@ const OWNER_AUTHORIZED_NO_TERMS = * url: string, * positions?: 'gps'|'event', * headers?: Record, headerClock?: 'untrusted', + * speedUnit?: 'km/h', * pollIntervalMs?: number, tripUpdatesUrl?: string, * tripUpdatesIntervalMs?: number, maxBodyBytes?: number, * limits?: {quote: string, note?: string}, @@ -1307,6 +1310,10 @@ export const RAIL_FEED_REGISTRY = Object.freeze([ format: 'gtfs-rt', // Every TaM vehicle (about 15 KB); the tram routes are kept. url: 'https://gtfsproxy.e-tam.fr/COMMON/VehiclePosition.pb', + // Speeds are km/h where the spec asks m/s (the Transit layer's entry for + // this feed measured readings at 3.3 times the movement); the proxy + // converts them. + speedUnit: 'km/h', tripUpdatesUrl: 'https://gtfsproxy.e-tam.fr/COMMON/TripUpdate.pb', tripUpdatesIntervalMs: 60_000, staticGtfsUrl: 'https://gtfsproxy.e-tam.fr/COMMON/GTFS.zip', diff --git a/src/data/railFeeds.test.mjs b/src/data/railFeeds.test.mjs index 5304680a9..f6fb8db1e 100644 --- a/src/data/railFeeds.test.mjs +++ b/src/data/railFeeds.test.mjs @@ -66,6 +66,7 @@ const REGISTRY_FIELDS = new Set([ 'positions', 'headers', 'headerClock', + 'speedUnit', 'pollIntervalMs', 'tripUpdatesUrl', 'tripUpdatesIntervalMs', @@ -472,3 +473,19 @@ test('feeds that read routes from their schedule pack can build one', () => { })) assert.ok(!/@/.test(value), feed.id); }); + +test('a feed Transit also reads declares the same speed unit', async () => { + const { TRANSIT_FEED_REGISTRY } = await import('./transitFeeds.js'); + const transitByUrl = new Map( + TRANSIT_FEED_REGISTRY.map((feed) => [feed.url, feed]), + ); + let shared = 0; + for (const feed of RAIL_FEED_REGISTRY) { + const transit = transitByUrl.get(feed.url); + if (!transit) continue; + shared += 1; + assert.equal(feed.speedUnit, transit.speedUnit, feed.id); + } + assert.ok(shared > 0); + assert.equal(getRegisteredRailFeed('tam-montpellier').speedUnit, 'km/h'); +});