From 2c1b2c278ddabf7eda6536558d60a37a6d52ac16 Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Mon, 9 Nov 2026 13:58:20 +0000 Subject: [PATCH] fix(transit): read Rouen and Le Havre from the operators' own feeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rouen now reads Métropole Rouen Normandie's TCAR vehicle feed (api.mrn.cityway.fr) and Le Havre LiA's own GTFS-RT (opendata.transports-lia.fr), both listed in their transport.data.gouv.fr records, instead of the unofficial bus-tracker.fr relay. LiA ids carry an 8- prefix, so a new liaRouteLabel strips it and liaRouteMode reads the line. --- DATA_SOURCES.md | 4 ++-- src/data/transitFeeds.js | 39 ++++++++++++++++++---------------- src/data/transitFeeds.test.mjs | 12 +++++++---- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index a302235fd..31753ade5 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -133,9 +133,9 @@ How to read this: | **Carris (Lisbon city)** (gateway.carris.pt) | Transit layer live vehicles, Lisbon, Portugal | No licence stated (https://mobilitydatabase.org/feeds/gtfs_rt/mdb-2930). Keyless official gateway; no licence found. Same basis as Metro Transit and OVapi. Carries neither bearing nor speed, and vehicle reports run about a minute old. Route ids append a variant. | Carris | | **BAS.MY Johor Bahru** (api.data.gov.my) | Transit layer live vehicles, Johor Bahru, Malaysia | CC BY 4.0 (https://developer.data.gov.my/realtime-api/gtfs-realtime). Stated: “All GTFS Realtime feeds are licensed under CC BY 4.0.” Served keyless by Malaysia’s national open-data API, which caps GTFS Realtime at 4 requests a minute and refreshes positions every 30 seconds, so the proxy caches this feed for 30 seconds. Without the trailing slash the API redirects. Route ids append an operator code. | BAS.MY / data.gov.my | | **BAS.MY Kota Bharu** (api.data.gov.my) | Transit layer live vehicles, Kota Bharu, Malaysia | CC BY 4.0 (https://developer.data.gov.my/realtime-api/gtfs-realtime). Same API, licence and rate limit as Johor Bahru. | BAS.MY / data.gov.my | -| **Rouen Astuce** (gtfs.bus-tracker.fr) | Transit layer live vehicles, Rouen, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-statiques-et-temps-reel-du-reseau-astuce-metropole-rouen-normandie). The metropolis publishes its realtime data under the Licence Ouverte (dataset record quoted); this registry reads it through bus-tracker.fr, an unofficial relay that merges the city’s networks into one GTFS-RT file. Route ids carry the network prefix (`TCAR:`); line 90 is the Métro (a street-running light rail, drawn as a tram) and 99 the Calypso boat. | Métropole Rouen Normandie (Astuce) / bus-tracker.fr | +| **Rouen Astuce** (api.mrn.cityway.fr) | Transit layer live vehicles, Rouen, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/donnees-statiques-et-temps-reel-du-reseau-astuce-metropole-rouen-normandie). The metropolis publishes its realtime data under the Licence Ouverte (dataset record quoted); and this is its own TCAR vehicle feed from that record; the suburban TAE and TNI networks are separate feeds and are not read. Route ids carry the network prefix (`TCAR:`); line 90 is the Métro (a street-running light rail, drawn as a tram) and 99 the Calypso boat. | Métropole Rouen Normandie (Astuce) | | **Toulon Mistral** (feed-rdtpm-toulon.ratpdev.com) | Transit layer live vehicles, Toulon, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/reseau-de-transport-urbain-de-la-metropole-toulon-provence-mediterranee). Dataset record’s licence field quoted. Speeds are km/h: readings ran 3.5 times the measured movement on 2026-11-07, so the proxy converts them. Route ids are zero-padded. | Métropole Toulon Provence Méditerranée (Mistral) | -| **Le Havre LiA** (gtfs.bus-tracker.fr) | Transit layer live vehicles, Le Havre, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-de-transport-en-commun-lia-gtfs-siri). The network publishes under ODbL (dataset record quoted); read through the bus-tracker.fr relay, which converts its SIRI feed to GTFS-RT. The layer draws positions and redistributes no database. Lines A and B are the tramway. | Le Havre Seine Métropole (LiA) / bus-tracker.fr | +| **Le Havre LiA** (opendata.transports-lia.fr) | Transit layer live vehicles, Le Havre, France | ODbL (https://transport.data.gouv.fr/datasets/reseau-de-transport-en-commun-lia-gtfs-siri). The network publishes under ODbL (dataset record quoted); and this is its own GTFS-RT vehicle feed from that record; route ids carry the network prefix `8-`. The layer draws positions and redistributes no database. Lines A and B are the tramway. | Le Havre Seine Métropole (LiA) | | **Tours Fil Bleu** (data.filbleu.fr) | Transit layer live vehicles, Tours, France | Licence Ouverte 2.0 (https://transport.data.gouv.fr/datasets/fil-bleu-syndicat-des-mobilites-gtfs-gtfs-rt). Dataset record’s licence field quoted. | Fil Bleu – Syndicat des Mobilités de Touraine | | **Radio Browser** | Geolocated internet-radio station directory and station-level tags | Public-domain directory data under PDDL 1.0; individual broadcaster stream terms apply | "Radio Browser" plus a link to the selected broadcaster | | **Re:Earth Terrain** (Mapterhorn) | Terrain (keyless globe stacks — OSM etc. — + `/api/terrain/heights` ellipsoidal-height lookups) | Terrain mesh: CC BY 4.0; geoid: EGM2008 (NGA, public domain) | "Terrain (keyless globe stacks): Re:Earth Terrain / Mapterhorn (CC BY 4.0) / EGM2008 (NGA)" | diff --git a/src/data/transitFeeds.js b/src/data/transitFeeds.js index 7f2d79a02..e9aeb4359 100644 --- a/src/data/transitFeeds.js +++ b/src/data/transitFeeds.js @@ -426,11 +426,14 @@ const actvRouteLabel = routeIdPattern(/^(.+)_[A-Z]{2}$/); const bcTransitRouteLabel = routeIdPattern(/^(.+)-VIC$/); /** Carris (Lisbon city) appends a variant: `172_1` is route 172. */ const carrisRouteLabel = routeIdPattern(/^(\w+)_\d+$/); -/** bus-tracker.fr prefixes the network: `TCAR:93` is Rouen line 93. */ -const busTrackerRouteLabel = routeIdPattern(/^[A-Z]+:(.+)$/); +/** Astuce prefixes the network: `TCAR:93` is Rouen line 93. */ +const astuceNetworkLabel = routeIdPattern(/^[A-Z]+:(.+)$/); /** BAS.MY Johor appends a fixed operator code: `J33CWLMYJB` is route J33. */ const mybasJohorRouteLabel = routeIdPattern(/^(.+?)CWLMYJB$/); +/** LiA prefixes its network: `8-C3` is Le Havre line C3, `8-09` line 9. */ +const liaRouteLabel = routeIdPattern(/^8-0*(\w.*)$/); + /** * Le Havre LiA runs its two tram lines as A and B and the funicular as * `FUNI`; every other line is a bus. @@ -439,15 +442,15 @@ const mybasJohorRouteLabel = routeIdPattern(/^(.+?)CWLMYJB$/); */ function liaRouteMode(routeId) { if (!routeId) return 'unknown'; - if (routeId === 'FUNI') return 'rail'; - return /^(?:[A-Z]+:)?[AB]$/.test(routeId) ? 'tram' : 'bus'; + const line = liaRouteLabel(routeId); + if (line === 'FUNI') return 'rail'; + return /^[AB]$/.test(line) ? 'tram' : 'bus'; } /** - * Rouen Astuce lines that are not buses, by bus-tracker.fr route id, from the - * relay's own `astuce-tcar.zip` routes.txt (checked 2026-11-09): line 90 is - * the Métro (GTFS type 1, a light rail that runs mostly on the street) and 99 - * the Calypso river boat. + * Rouen Astuce lines that are not buses, by route id, from the TCAR + * routes.txt (checked 2026-11-09): line 90 is the Métro (GTFS type 1, a light + * rail that runs mostly on the street) and 99 the Calypso river boat. */ const ASTUCE_ROUTES = Object.freeze({ 'TCAR:90': Object.freeze({ mode: 'tram', label: 'Métro' }), @@ -462,7 +465,7 @@ function astuceRouteMode(routeId) { /** `TCAR:93` is Rouen line 93; the Métro and Calypso keep their names. */ function astuceRouteLabel(routeId) { - return ASTUCE_ROUTES[routeId ?? '']?.label ?? busTrackerRouteLabel(routeId); + return ASTUCE_ROUTES[routeId ?? '']?.label ?? astuceNetworkLabel(routeId); } /** @@ -1504,18 +1507,18 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([ transitFeed({ id: 'astuce-rouen', name: 'Rouen Astuce', - operator: 'Métropole Rouen Normandie (Astuce), relayed by bus-tracker.fr', + operator: 'Métropole Rouen Normandie (Astuce), réseau TCAR', region: 'Rouen, France', center: { lat: 49.44, lon: 1.1 }, loadRadiusKm: 20, - url: 'https://gtfs.bus-tracker.fr/gtfs-rt/tcar/?tcar=1&tae=1&tni=1', + url: 'https://api.mrn.cityway.fr/dataflow/vehicle-tc-tr/download?provider=TCAR&dataFormat=gtfs-rt', license: 'Licence Ouverte 2.0', licenseUrl: 'https://transport.data.gouv.fr/datasets/donnees-statiques-et-temps-reel-du-reseau-astuce-metropole-rouen-normandie', - attribution: 'Métropole Rouen Normandie (Astuce) / bus-tracker.fr', + attribution: 'Métropole Rouen Normandie (Astuce)', terms: { quote: 'licence: lov2', - note: 'The metropolis publishes its realtime data under the Licence Ouverte (dataset record quoted); this registry reads it through bus-tracker.fr, an unofficial relay that merges the city’s networks into one GTFS-RT file. Route ids carry the network prefix (`TCAR:`); `TCAR:90` is the Métro (a street-running light rail, drawn as a tram) and `TCAR:99` the Calypso boat.', + note: 'The metropolis publishes its realtime data under the Licence Ouverte (dataset record quoted), and this is its own TCAR vehicle feed from that record (178 vehicles on 2026-11-09). The suburban TAE and TNI networks are separate feeds in the same record and are not read here. Route ids carry the network prefix (`TCAR:`); `TCAR:90` is the Métro (a street-running light rail, drawn as a tram) and `TCAR:99` the Calypso boat.', }, defaultMode: 'bus', routeLabel: astuceRouteLabel, @@ -1544,21 +1547,21 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([ transitFeed({ id: 'lia-le-havre', name: 'Le Havre LiA', - operator: 'Le Havre Seine Métropole (LiA), relayed by bus-tracker.fr', + operator: 'Le Havre Seine Métropole (LiA)', region: 'Le Havre, France', center: { lat: 49.49, lon: 0.11 }, loadRadiusKm: 18, - url: 'https://gtfs.bus-tracker.fr/gtfs-rt/lia/vehicle-positions', + url: 'https://opendata.transports-lia.fr/gtfs-rt/VehiclePosition.pb', license: 'ODbL', licenseUrl: 'https://transport.data.gouv.fr/datasets/reseau-de-transport-en-commun-lia-gtfs-siri', - attribution: 'Le Havre Seine Métropole (LiA) / bus-tracker.fr', + attribution: 'Le Havre Seine Métropole (LiA)', terms: { quote: 'licence: odc-odbl', - note: 'The network publishes under ODbL (dataset record quoted); read through the bus-tracker.fr relay, which converts its SIRI feed to GTFS-RT. The layer draws positions and redistributes no database. Lines A and B are the tramway.', + note: 'The network publishes under ODbL (dataset record quoted), and this is its own GTFS-RT vehicle feed from that record (105 vehicles on 2026-11-09). The layer draws positions and redistributes no database. Route ids carry the network prefix `8-`; lines A and B are the tramway.', }, defaultMode: 'bus', - routeLabel: busTrackerRouteLabel, + routeLabel: liaRouteLabel, routeMode: liaRouteMode, }), transitFeed({ diff --git a/src/data/transitFeeds.test.mjs b/src/data/transitFeeds.test.mjs index 3867b1225..75a902728 100644 --- a/src/data/transitFeeds.test.mjs +++ b/src/data/transitFeeds.test.mjs @@ -702,8 +702,8 @@ test('trams, waterbuses and buses are told apart where the ids allow it', () => assert.equal(mode('pid-praha', 'L22'), 'tram'); assert.equal(mode('pid-praha', 'L135'), 'bus'); assert.equal(mode('tbm-bordeaux', '16'), 'bus'); - assert.equal(mode('lia-le-havre', 'B'), 'tram'); - assert.equal(mode('lia-le-havre', '3'), 'bus'); + assert.equal(mode('lia-le-havre', '8-B'), 'tram'); + assert.equal(mode('lia-le-havre', '8-C3'), 'bus'); assert.equal(mode('ztp-krakow-tram', null), 'tram'); assert.equal(mode('actv-venezia-waterbus', '1_UN'), 'ferry'); }); @@ -759,14 +759,18 @@ test('Bordeaux trams ride numbered route ids and keep their letters', () => { assert.equal(transitRouteLabel(feed, '01'), '1'); }); -test('Rouen Métro and Calypso are not buses', () => { +test('Rouen Métro and Calypso are not buses; Le Havre reads its own ids', () => { const feed = getTransitFeed('astuce-rouen'); assert.equal(transitModeFor(feed, 'TCAR:90'), 'tram'); assert.equal(transitRouteLabel(feed, 'TCAR:90'), 'Métro'); assert.equal(transitModeFor(feed, 'TCAR:99'), 'ferry'); assert.equal(transitModeFor(feed, 'TCAR:93'), 'bus'); assert.equal(transitRouteLabel(feed, 'TAE:F'), 'F'); - assert.equal(transitModeFor(getTransitFeed('lia-le-havre'), 'FUNI'), 'rail'); + const lia = getTransitFeed('lia-le-havre'); + assert.equal(transitModeFor(lia, '8-FUNI'), 'rail'); + assert.equal(transitRouteLabel(lia, '8-C3'), 'C3'); + assert.equal(transitRouteLabel(lia, '8-09'), '9'); + assert.equal(transitRouteLabel(lia, '8-A'), 'A'); }); test('data.gov.my feeds ask the proxy for no more than its 4 a minute', () => {