/g,
+ `
`
+ );
+
+ // To handle the marquee for "CAR DE REMPLACEMENT"
+ // find: `let infoText = infoCause || cause;`
+ content = content.replace(
+ /let infoText = infoCause \|\| cause;/g,
+ "let infoText = infoCause || cause;\n\t\tif (isSub) infoText = infoText ? 'CAR DE REMPLACEMENT - ' + infoText : 'CAR DE REMPLACEMENT';"
+ );
+
+ fs.writeFileSync(file, content, 'utf8');
+}
+
+patchFile('src/pages/afficheurs/classiques/ClassiquesDeparts.js');
+patchFile('src/pages/afficheurs/classiques/ClassiquesArrivees.js');
diff --git a/patch-helper.js b/patch-helper.js
new file mode 100644
index 0000000..674177d
--- /dev/null
+++ b/patch-helper.js
@@ -0,0 +1,77 @@
+const fs = require('fs');
+let content = fs.readFileSync('src/utils/trainLogoHelper.js', 'utf8');
+
+if (!content.includes('import { resolveRegionByNumber }')) {
+ content = "import { resolveRegionByNumber } from './gtfsRegionalRanges';\n" + content;
+}
+
+const targetStart = " // Application du plan de num";
+const startIndex = content.indexOf(targetStart);
+if (startIndex !== -1) {
+ const newLogic = ` let isCar = opNorm.includes('car') || opNorm.includes('autocar') || opNorm.includes('bus');
+ let isSubstitution = false;
+ let brand = typeLabel;
+
+ // 4. Application du plan de numérotation déterministe depuis GTFS (nouveau moteur)
+ if ((typeLabel === 'TER' || typeLabel === 'AUTOCAR' || typeLabel === 'Car TER' || typeLabel === 'Car' || typeLabel === 'Autocar' || opRaw.toUpperCase().includes('CAR')) && numTrain) {
+ const regionMatch = resolveRegionByNumber(numTrain);
+ if (regionMatch) {
+ if (regionMatch.isCar) {
+ isCar = true;
+ typeLabel = 'Car TER'; // Forcer l'affichage CAR
+ }
+ if (regionMatch.isSubstitution) {
+ isSubstitution = true;
+ }
+
+ brand = regionMatch.brand;
+
+ // Affiner le typeLabel selon la marque si train
+ if (!isCar) {
+ switch (brand) {
+ case 'FLUO': typeLabel = 'FLUO'; break;
+ case 'TER HDF': typeLabel = 'TER HDF'; break;
+ case 'NOMAD': typeLabel = 'NOMAD'; break;
+ case 'BREIZHGO': typeLabel = 'BREIZHGO'; break;
+ case 'ALEOP': typeLabel = 'ALÉOP'; break;
+ case 'REMI': typeLabel = 'RÉMI'; break;
+ case 'TER NA': typeLabel = 'TER NA'; break;
+ case 'LIO': typeLabel = 'LiO'; break;
+ case 'ZOU': typeLabel = 'ZOU !'; break;
+ case 'MOBIGO': typeLabel = 'MOBIGO'; break;
+ case 'TER AURA': typeLabel = 'TER AURA'; break;
+ default: typeLabel = 'TER';
+ }
+ }
+
+ // Tenter de charger le logo SVG de la marque si ce n'est pas un car
+ if (!isCar) {
+ const slugMatch = logoLookup.get(brand.toLowerCase().replace(/[^a-z]/g, ''));
+ if (slugMatch) {
+ matchedLogo = slugMatch;
+ } else if (brand === 'TER AURA') {
+ matchedLogo = logoLookup.get('terrhonealpes');
+ } else if (brand === 'TER NA') {
+ matchedLogo = logoLookup.get('teraquitaine');
+ } else if (brand === 'TER HDF') {
+ matchedLogo = logoLookup.get('hautsdefrance');
+ }
+ } else {
+ // Pour les cars régionaux, on associe l'icône de car
+ matchedLogo = logoLookup.get('trackcar') || { file: 'track-car.svg', name: 'Car TER' };
+ }
+ }
+ }
+
+ const logoFile = (matchedLogo && matchedLogo.file) || (isCar ? 'track-car.svg' : 'sncf-voyageurs-logo.png');
+ const logoSrc = \`/type/\${logoFile}\`;
+
+ return { logoSrc, typeLabel, numTrain, isCar, isSubstitution, brand };
+}
+`;
+
+ content = content.substring(0, startIndex) + newLogic;
+ fs.writeFileSync('src/utils/trainLogoHelper.js', content, 'utf8');
+} else {
+ console.log('Target block not found');
+}
diff --git a/src/pages/afficheurs/classiques/ClassiquesArrivees.css b/src/pages/afficheurs/classiques/ClassiquesArrivees.css
index 7317283..68e05d3 100644
--- a/src/pages/afficheurs/classiques/ClassiquesArrivees.css
+++ b/src/pages/afficheurs/classiques/ClassiquesArrivees.css
@@ -433,4 +433,16 @@
color: var(--yellow) !important;
margin-top: 1px;
margin-left: 2px;
-}
\ No newline at end of file
+}
+
+.brand-lio { background-color: #e3001b !important; color: white !important; font-weight: bold; }
+.brand-fluo { background-color: #f78bb8 !important; color: white !important; }
+.brand-mobigo { background-color: #cfd82c !important; color: black !important; }
+.brand-remi { background-color: #00874e !important; color: white !important; }
+.brand-nomad { background-color: #6bb07a !important; color: white !important; }
+.brand-breizhgo { background-color: #00c6d3 !important; color: black !important; }
+.brand-aleop { background-color: #00b0ca !important; color: white !important; }
+.brand-terna { background-color: #ea5b0c !important; color: white !important; }
+.brand-teraura { background-color: #0077c8 !important; color: white !important; }
+.brand-zou { background-color: #0055a4 !important; color: white !important; }
+.brand-terhdf { background-color: #c9e000 !important; color: black !important; }
\ No newline at end of file
diff --git a/src/pages/afficheurs/classiques/ClassiquesArrivees.js b/src/pages/afficheurs/classiques/ClassiquesArrivees.js
index 23c5a74..664e725 100644
--- a/src/pages/afficheurs/classiques/ClassiquesArrivees.js
+++ b/src/pages/afficheurs/classiques/ClassiquesArrivees.js
@@ -1254,7 +1254,7 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
}
const horaire = h || {};
- const { logoSrc, typeLabel, numTrain } = getTrainLogoAndMeta(horaire);
+ const { logoSrc, typeLabel, numTrain, isCar: metaIsCar, isSubstitution: metaIsSubstitution, brand: metaBrand } = getTrainLogoAndMeta(horaire);
const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.arrival_time || horaire.time || horaire.heure || '');
const origin = horaire.gare_depart_nom || horaire.gare_depart || horaire.origine || '';
const formattedOrigin = formatStationName(origin);
diff --git a/src/pages/afficheurs/classiques/ClassiquesDeparts.css b/src/pages/afficheurs/classiques/ClassiquesDeparts.css
index 3227e3f..7d80495 100644
--- a/src/pages/afficheurs/classiques/ClassiquesDeparts.css
+++ b/src/pages/afficheurs/classiques/ClassiquesDeparts.css
@@ -433,4 +433,16 @@
color: var(--yellow) !important;
margin-top: 1px;
margin-left: 2px;
-}
\ No newline at end of file
+}
+
+.brand-lio { background-color: #e3001b !important; color: white !important; font-weight: bold; }
+.brand-fluo { background-color: #f78bb8 !important; color: white !important; }
+.brand-mobigo { background-color: #cfd82c !important; color: black !important; }
+.brand-remi { background-color: #00874e !important; color: white !important; }
+.brand-nomad { background-color: #6bb07a !important; color: white !important; }
+.brand-breizhgo { background-color: #00c6d3 !important; color: black !important; }
+.brand-aleop { background-color: #00b0ca !important; color: white !important; }
+.brand-terna { background-color: #ea5b0c !important; color: white !important; }
+.brand-teraura { background-color: #0077c8 !important; color: white !important; }
+.brand-zou { background-color: #0055a4 !important; color: white !important; }
+.brand-terhdf { background-color: #c9e000 !important; color: black !important; }
\ No newline at end of file
diff --git a/src/pages/afficheurs/classiques/ClassiquesDeparts.js b/src/pages/afficheurs/classiques/ClassiquesDeparts.js
index d862670..b31298f 100644
--- a/src/pages/afficheurs/classiques/ClassiquesDeparts.js
+++ b/src/pages/afficheurs/classiques/ClassiquesDeparts.js
@@ -1253,7 +1253,7 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
}
const horaire = h || {};
- const { logoSrc, typeLabel, numTrain } = getTrainLogoAndMeta(horaire);
+ const { logoSrc, typeLabel, numTrain, isCar: metaIsCar, isSubstitution: metaIsSubstitution, brand: metaBrand } = getTrainLogoAndMeta(horaire);
const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.departure_time || horaire.time || horaire.heure || '');
const destination = horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination || '';
const formattedDestination = formatStationName(destination);
diff --git a/src/services/sncfApi.js b/src/services/sncfApi.js
index 3656cc9..e7a0700 100644
--- a/src/services/sncfApi.js
+++ b/src/services/sncfApi.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
// Service d'accès aux données SNCF (GTFS / GTFS-RT / SIRI Lite / Open Data SNCF)
import { transit_realtime } from 'gtfs-realtime-bindings';
import { findGtfsGare, searchGtfsGares } from './gtfsStations';
@@ -404,153 +405,11 @@ async function fetchSituationExchangeData() {
* Récupère les quais en temps réel depuis l'API OpenData SNCF (Navitia + OpenDataSoft)
*/
async function fetchOpenDataSncfPlatforms(uic, mode = 'departs') {
- if (!uic) return new Map();
- const cacheKey = `${uic}_${mode}`;
- const now = Date.now();
- if (openDataPlatformsCache.has(cacheKey)) {
- const cached = openDataPlatformsCache.get(cacheKey);
- if (now - cached.timestamp < 30000) {
- return cached.platforms;
- }
- }
-
- const platformsMap = new Map();
-
- try {
- const authHeader = NAVITIA_TOKEN.startsWith('Basic') ? NAVITIA_TOKEN : 'Basic ' + btoa(NAVITIA_TOKEN + ':');
- const uicClean = String(uic).replace(/\D/g, '');
- const isArrival = mode === 'arrivees';
- const navitiaPath = isArrival ? 'arrivals' : 'departures';
- const odsDataset = isArrival ? 'prochaines-arrivees-des-trains-en-gare-temps-reel' : 'prochains-departs-des-trains-en-gare-temps-reel';
-
- const navitiaEndpoints = [
- `https://api.sncf.com/v1/coverage/sncf/stop_areas/stop_area:OCE:SA:${uicClean}/${navitiaPath}?count=100`,
- `https://api.sncf.com/v1/coverage/sncf/stop_areas/stop_area:SNCF:${uicClean}/${navitiaPath}?count=100`,
- `https://api.sncf.com/v1/coverage/sncf/stop_points/stop_point:OCE:SP:${uicClean}/${navitiaPath}?count=100`,
- `https://api.navitia.io/v1/coverage/sncf/stop_areas/stop_area:OCE:SA:${uicClean}/${navitiaPath}?count=100`,
- `https://api.navitia.io/v1/coverage/sncf/stop_areas/stop_area:${uicClean}/${navitiaPath}?count=100`
- ];
-
- const [navitiaRes, odsV2Res, odsV1Res] = await Promise.allSettled([
- fetchWithTimeout(navitiaEndpoints[0], { headers: { 'Authorization': authHeader } }, 5000)
- .then(async res => {
- if (res && res.ok) return res;
- return fetchWithTimeout(navitiaEndpoints[1], { headers: { 'Authorization': authHeader } }, 5000);
- })
- .then(async res => {
- if (res && res.ok) return res;
- return fetchWithTimeout(navitiaEndpoints[3], { headers: { 'Authorization': NAVITIA_TOKEN } }, 5000);
- }),
- fetchWithTimeout(`https://ressources.data.sncf.com/api/explore/v2.1/catalog/datasets/${odsDataset}/records?limit=100&where=search(code_uic%2C%22${uicClean}%22)`, {}, 5000),
- fetchWithTimeout(`https://data.sncf.com/api/records/1.0/search/?dataset=${odsDataset}&q=${uicClean}&rows=100`, {}, 5000)
- ]);
-
- // 1. Décodage Navitia (API OpenData SNCF temps réel)
- if (navitiaRes.status === 'fulfilled' && navitiaRes.value && navitiaRes.value.ok) {
- const data = await navitiaRes.value.json();
- const list = (isArrival ? data.arrivals : data.departures) || [];
- for (const item of list) {
- const info = item.display_informations || {};
- const trainNum = cleanNum(info.headsign || info.trip_short_name || info.commercial_mode || info.code || '');
- const rawNum = String(info.headsign || info.trip_short_name || info.code || '').replace(/\D/g, '');
-
- const stopPoint = item.stop_point || (item.stop_date_time && item.stop_date_time.stop_point) || {};
- let platform = (
- stopPoint.platform_code ||
- info.platform ||
- ''
- ).toString().trim();
-
- if (!platform && stopPoint.label) {
- const m = stopPoint.label.match(/(?:voie|quai|track|platform)\s*([a-z0-9]+)/i);
- if (m) platform = m[1];
- }
-
- platform = platform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
-
- const destClean = info.direction ? formatStationName(info.direction).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
-
- if (platform) {
- if (trainNum && uicClean) {
- platformsMap.set(`${trainNum}_${uicClean}`, platform);
- platformsMap.set(`${uicClean}_${trainNum}`, platform);
- }
- if (rawNum && uicClean) {
- platformsMap.set(`${rawNum}_${uicClean}`, platform);
- platformsMap.set(`${uicClean}_${rawNum}`, platform);
- }
- if (trainNum && destClean) {
- platformsMap.set(`${trainNum}_${destClean}`, platform);
- }
- }
- }
- }
-
- // 2. Décodage OpenDataSoft SNCF v2.1
- if (odsV2Res.status === 'fulfilled' && odsV2Res.value && odsV2Res.value.ok) {
- const data = await odsV2Res.value.json();
- const records = data.results || data.records || [];
- for (const r of records) {
- const fields = r.fields || r;
- const trainNum = cleanNum(fields.num || fields.numero || fields.train || fields.num_train || '');
- const rawNum = String(fields.num || fields.numero || fields.train || fields.num_train || '').replace(/\D/g, '');
- const platform = String(fields.voie || fields.quai || fields.platform || fields.track || fields.voie_quai || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
- const destClean = fields.destination || fields.orig ? formatStationName(fields.destination || fields.orig).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
-
- if (platform) {
- if (trainNum && uicClean) {
- platformsMap.set(`${trainNum}_${uicClean}`, platform);
- platformsMap.set(`${uicClean}_${trainNum}`, platform);
- }
- if (rawNum && uicClean) {
- platformsMap.set(`${rawNum}_${uicClean}`, platform);
- platformsMap.set(`${uicClean}_${rawNum}`, platform);
- }
- if (trainNum && destClean) {
- platformsMap.set(`${trainNum}_${destClean}`, platform);
- }
- }
- }
- }
-
- // 3. Décodage OpenDataSoft SNCF v1.0
- if (odsV1Res.status === 'fulfilled' && odsV1Res.value && odsV1Res.value.ok) {
- const data = await odsV1Res.value.json();
- const records = data.records || [];
- for (const r of records) {
- const fields = r.fields || {};
- const trainNum = cleanNum(fields.num || fields.numero || fields.train || fields.num_train || '');
- const rawNum = String(fields.num || fields.numero || fields.train || fields.num_train || '').replace(/\D/g, '');
- const platform = String(fields.voie || fields.quai || fields.platform || fields.track || fields.voie_quai || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
- const destClean = fields.destination || fields.orig ? formatStationName(fields.destination || fields.orig).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
-
- if (platform) {
- if (trainNum && uicClean) {
- platformsMap.set(`${trainNum}_${uicClean}`, platform);
- platformsMap.set(`${uicClean}_${trainNum}`, platform);
- }
- if (rawNum && uicClean) {
- platformsMap.set(`${rawNum}_${uicClean}`, platform);
- platformsMap.set(`${uicClean}_${rawNum}`, platform);
- }
- if (trainNum && destClean) {
- platformsMap.set(`${trainNum}_${destClean}`, platform);
- }
- }
- }
- }
- } catch (err) {
- console.warn('[sncfApi] Notice: OpenData SNCF platform fetch error:', err);
- }
-
- openDataPlatformsCache.set(cacheKey, {
- timestamp: now,
- platforms: platformsMap
- });
-
- return platformsMap;
+ // L'API api.sncf.com v1 et data.sncf.com (v1) sont obsolètes et retournent des erreurs 401/404.
+ // Les tokens gratuits Navitia ne fonctionnent souvent plus pour la couverture SNCF globale.
+ // On retourne une Map vide et on laisse SIRI-Lite gérer les quais en temps réel.
+ return new Map();
}
-
/**
* Récupère et décode les mises à jour temps réel GTFS-RT SNCF
*/
diff --git a/src/utils/gtfsRegionalRanges.js b/src/utils/gtfsRegionalRanges.js
new file mode 100644
index 0000000..269a1a1
--- /dev/null
+++ b/src/utils/gtfsRegionalRanges.js
@@ -0,0 +1,62 @@
+export const REGIONAL_RANGES = [
+ // --- CARS DE SUBSTITUTION & CARS REGULIERS (Route type 2 & 3) ---
+ { min: 404000, max: 412999, regionCode: 'AURA', brand: 'TER', isCar: true, isSubstitution: true },
+ { min: 415035, max: 424403, regionCode: 'BFC', brand: 'MOBIGO', isCar: true, isSubstitution: true },
+ { min: 421001, max: 422112, regionCode: 'BRE', brand: 'BREIZHGO', isCar: true, isSubstitution: true },
+ { min: 424020, max: 426502, regionCode: 'CVL', brand: 'REMI', isCar: true, isSubstitution: true },
+ { min: 427250, max: 435837, regionCode: 'GE', brand: 'FLUO', isCar: true, isSubstitution: true },
+ { min: 436010, max: 439537, regionCode: 'HDF', brand: 'TER', isCar: true, isSubstitution: true },
+ { min: 442108, max: 447248, regionCode: 'NOR', brand: 'NOMAD', isCar: true, isSubstitution: true },
+ { min: 448014, max: 455684, regionCode: 'NA', brand: 'TER', isCar: true, isSubstitution: true },
+ { min: 412062, max: 412494, regionCode: 'OCC', brand: 'LIO', isCar: true, isSubstitution: true },
+ { min: 455005, max: 462221, regionCode: 'OCC', brand: 'LIO', isCar: true, isSubstitution: true },
+ { min: 464040, max: 464140, regionCode: 'PDL', brand: 'ALEOP', isCar: true, isSubstitution: true },
+ { min: 466752, max: 468300, regionCode: 'PACA', brand: 'ZOU', isCar: true, isSubstitution: true },
+
+ // Cars TER réguliers 5 chiffres
+ { min: 30131, max: 31478, regionCode: 'AURA', brand: 'TER', isCar: true, isSubstitution: false },
+ { min: 33155, max: 35072, regionCode: 'AURA', brand: 'TER', isCar: true, isSubstitution: false },
+ { min: 35000, max: 37529, regionCode: 'BFC', brand: 'MOBIGO', isCar: true, isSubstitution: false },
+ { min: 43001, max: 45141, regionCode: 'GE', brand: 'FLUO', isCar: true, isSubstitution: false },
+ { min: 68130, max: 68283, regionCode: 'GE', brand: 'FLUO', isCar: true, isSubstitution: false },
+
+ // --- TRAINS TER (Séries 6 chiffres) ---
+ { min: 830102, max: 840877, regionCode: 'GE', brand: 'FLUO', isCar: false, isSubstitution: false },
+ { min: 841200, max: 849966, regionCode: 'HDF', brand: 'TER', isCar: false, isSubstitution: false },
+ { min: 850016, max: 853728, regionCode: 'NOR', brand: 'NOMAD', isCar: false, isSubstitution: false },
+ { min: 854007, max: 858397, regionCode: 'BRE', brand: 'BREIZHGO', isCar: false, isSubstitution: false },
+ { min: 857000, max: 862589, regionCode: 'PDL', brand: 'ALEOP', isCar: false, isSubstitution: false }, // Chevauchement partiel, géré par l'ordre
+ { min: 860001, max: 863714, regionCode: 'CVL', brand: 'REMI', isCar: false, isSubstitution: false },
+ { min: 863031, max: 869129, regionCode: 'NA', brand: 'TER', isCar: false, isSubstitution: false },
+ { min: 870000, max: 878991, regionCode: 'OCC', brand: 'LIO', isCar: false, isSubstitution: false },
+ { min: 879102, max: 881699, regionCode: 'PACA', brand: 'ZOU', isCar: false, isSubstitution: false },
+ { min: 882103, max: 890392, regionCode: 'AURA', brand: 'TER', isCar: false, isSubstitution: false },
+ { min: 891000, max: 895975, regionCode: 'BFC', brand: 'MOBIGO', isCar: false, isSubstitution: false },
+
+ // --- TRAINS TER (Séries 5 chiffres non ambiguës) ---
+ { min: 96500, max: 96769, regionCode: 'AURA', brand: 'TER', isCar: false, isSubstitution: false },
+ { min: 17750, max: 18146, regionCode: 'BFC', brand: 'MOBIGO', isCar: false, isSubstitution: false },
+ { min: 81390, max: 81496, regionCode: 'GE', brand: 'FLUO', isCar: false, isSubstitution: false },
+ { min: 86380, max: 88877, regionCode: 'GE', brand: 'FLUO', isCar: false, isSubstitution: false },
+ { min: 96100, max: 96299, regionCode: 'GE', brand: 'FLUO', isCar: false, isSubstitution: false },
+ { min: 16350, max: 16480, regionCode: 'HDF', brand: 'TER', isCar: false, isSubstitution: false },
+ { min: 19701, max: 19979, regionCode: 'HDF', brand: 'TER', isCar: false, isSubstitution: false },
+ { min: 13100, max: 13191, regionCode: 'NOR', brand: 'NOMAD', isCar: false, isSubstitution: false },
+];
+
+/**
+ * Déduit la région et la marque (train/car) à partir d'un numéro GTFS
+ * @param {string|number} trainNumStr
+ * @returns {Object|null}
+ */
+export function resolveRegionByNumber(trainNumStr) {
+ const num = parseInt(String(trainNumStr).replace(/\D/g, ''), 10);
+ if (isNaN(num)) return null;
+
+ for (const range of REGIONAL_RANGES) {
+ if (num >= range.min && num <= range.max) {
+ return range;
+ }
+ }
+ return null;
+}
diff --git a/src/utils/trainLogoHelper.js b/src/utils/trainLogoHelper.js
index 3f2495f..75b0f76 100644
--- a/src/utils/trainLogoHelper.js
+++ b/src/utils/trainLogoHelper.js
@@ -1,3 +1,4 @@
+import { resolveRegionByNumber } from './gtfsRegionalRanges';
// Indexation optimisée des logos pour recherche rapide et insensible à la casse / accents
const normalizeStr = (s) => {
if (!s) return '';
@@ -200,50 +201,63 @@ export function getTrainLogoAndMeta(horaire) {
typeLabel = 'TER';
}
- // Application du plan de numérotation déterministe TER
- if ((typeLabel === 'TER' || typeLabel === 'AUTOCAR' || typeLabel === 'Car TER') && numTrain) {
- const cleanNum = numTrain.replace(/\D/g, '');
- let matchedBrandSlug = null;
- let matchedBrandLabel = null;
+ let isCar = opNorm.includes('car') || opNorm.includes('autocar') || opNorm.includes('bus');
+ let isSubstitution = false;
+ let brand = typeLabel;
- if (cleanNum.length >= 4) {
- const p3 = parseInt(cleanNum.substring(0, 3), 10);
- const p4 = parseInt(cleanNum.substring(0, 4), 10);
+ // 4. Application du plan de numérotation déterministe depuis GTFS (nouveau moteur)
+ if ((typeLabel === 'TER' || typeLabel === 'AUTOCAR' || typeLabel === 'Car TER' || typeLabel === 'Car' || typeLabel === 'Autocar' || opRaw.toUpperCase().includes('CAR')) && numTrain) {
+ const regionMatch = resolveRegionByNumber(numTrain);
+ if (regionMatch) {
+ if (regionMatch.isCar) {
+ isCar = true;
+ typeLabel = 'Car TER'; // Forcer l'affichage CAR
+ }
+ if (regionMatch.isSubstitution) {
+ isSubstitution = true;
+ }
+
+ brand = regionMatch.brand;
- if ((p3 >= 830 && p3 <= 840) || (p3 >= 160 && p3 <= 162)) {
- matchedBrandSlug = 'fluo'; matchedBrandLabel = 'FLUO';
- } else if ((p3 >= 841 && p3 <= 849) || p3 === 163 || p3 === 164) {
- matchedBrandSlug = 'hautsdefrance'; matchedBrandLabel = 'TER HDF';
- } else if ((p3 >= 850 && p3 <= 853) || p3 === 165) {
- matchedBrandSlug = 'nomad'; matchedBrandLabel = 'NOMAD';
- } else if ((p3 >= 854 && p3 <= 856) || (p4 >= 1660 && p4 <= 1664)) {
- matchedBrandSlug = 'breizhgo'; matchedBrandLabel = 'BREIZHGO';
- } else if ((p3 >= 857 && p3 <= 859) || (p4 >= 1665 && p4 <= 1674)) {
- matchedBrandSlug = 'aleop'; matchedBrandLabel = 'ALÉOP';
- } else if ((p3 >= 860 && p3 <= 862) || (p4 >= 1675 && p4 <= 1684)) {
- matchedBrandSlug = 'remi'; matchedBrandLabel = 'RÉMI';
- } else if ((p3 >= 863 && p3 <= 869) || (p4 >= 1685 && p4 <= 1709)) {
- matchedBrandSlug = 'teraquitaine'; matchedBrandLabel = 'TER NA';
- } else if ((p3 >= 870 && p3 <= 872) || (p3 >= 876 && p3 <= 878) || (p4 >= 1710 && p4 <= 1719) || (p4 >= 1730 && p4 <= 1734)) {
- matchedBrandSlug = 'lio'; matchedBrandLabel = 'liO';
- } else if ((p3 >= 873 && p3 <= 875) || (p3 >= 882 && p3 <= 890) || (p4 >= 1720 && p4 <= 1729) || (p4 >= 1750 && p4 <= 1774) || (p4 >= 1790 && p4 <= 1799)) {
- matchedBrandSlug = 'ter-rhone-alpes'; matchedBrandLabel = 'TER AURA';
- } else if ((p3 >= 879 && p3 <= 881) || (p4 >= 1735 && p4 <= 1749)) {
- matchedBrandSlug = 'zou'; matchedBrandLabel = 'ZOU !';
- } else if ((p3 >= 891 && p3 <= 895) || (p4 >= 1775 && p4 <= 1789)) {
- matchedBrandSlug = 'mobigo'; matchedBrandLabel = 'MOBIGO';
+ // Affiner le typeLabel selon la marque si train
+ if (!isCar) {
+ switch (brand) {
+ case 'FLUO': typeLabel = 'FLUO'; break;
+ case 'TER HDF': typeLabel = 'TER HDF'; break;
+ case 'NOMAD': typeLabel = 'NOMAD'; break;
+ case 'BREIZHGO': typeLabel = 'BREIZHGO'; break;
+ case 'ALEOP': typeLabel = 'ALÉOP'; break;
+ case 'REMI': typeLabel = 'RÉMI'; break;
+ case 'TER NA': typeLabel = 'TER NA'; break;
+ case 'LIO': typeLabel = 'LiO'; break;
+ case 'ZOU': typeLabel = 'ZOU !'; break;
+ case 'MOBIGO': typeLabel = 'MOBIGO'; break;
+ case 'TER AURA': typeLabel = 'TER AURA'; break;
+ default: typeLabel = 'TER';
+ }
}
- if (matchedBrandSlug && matchedBrandLabel) {
- typeLabel = matchedBrandLabel;
- if (!matchedLogo || matchedLogo.file === 'logo-sncf.svg' || matchedLogo.file === 'logo-sncf.svg') {
- matchedLogo = logoLookup.get(matchedBrandSlug) || matchedLogo;
+ // Tenter de charger le logo SVG de la marque si ce n'est pas un car
+ if (!isCar) {
+ const slugMatch = logoLookup.get(brand.toLowerCase().replace(/[^a-z]/g, ''));
+ if (slugMatch) {
+ matchedLogo = slugMatch;
+ } else if (brand === 'TER AURA') {
+ matchedLogo = logoLookup.get('terrhonealpes');
+ } else if (brand === 'TER NA') {
+ matchedLogo = logoLookup.get('teraquitaine');
+ } else if (brand === 'TER HDF') {
+ matchedLogo = logoLookup.get('hautsdefrance');
}
+ } else {
+ // Pour les cars régionaux, on associe l'icône de car
+ matchedLogo = logoLookup.get('trackcar') || { file: 'track-car.svg', name: 'Car TER' };
}
}
}
- const logoFile = (matchedLogo && matchedLogo.file) || 'sncf-voyageurs-logo.png';
+
+ const logoFile = (matchedLogo && matchedLogo.file) || (isCar ? 'track-car.svg' : 'sncf-voyageurs-logo.png');
const logoSrc = `/type/${logoFile}`;
- return { logoSrc, typeLabel, numTrain };
+ return { logoSrc, typeLabel, numTrain, isCar, isSubstitution, brand };
}