From eb198cc72765646376284892b7f448c323fb1f46 Mon Sep 17 00:00:00 2001 From: Admin Git Date: Wed, 7 Oct 2026 17:14:40 +0200 Subject: [PATCH] V1.1.1 // MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -> Patch du bug du logo de l'horaire pas affiché. Corrigé pour les Horaires TER à 6 numéros. --- patch-displays.js | 48 ++++++ patch-helper.js | 77 +++++++++ .../classiques/ClassiquesArrivees.css | 14 +- .../classiques/ClassiquesArrivees.js | 2 +- .../classiques/ClassiquesDeparts.css | 14 +- .../classiques/ClassiquesDeparts.js | 2 +- src/services/sncfApi.js | 151 +----------------- src/utils/gtfsRegionalRanges.js | 62 +++++++ src/utils/trainLogoHelper.js | 86 +++++----- 9 files changed, 270 insertions(+), 186 deletions(-) create mode 100644 patch-displays.js create mode 100644 patch-helper.js create mode 100644 src/utils/gtfsRegionalRanges.js diff --git a/patch-displays.js b/patch-displays.js new file mode 100644 index 0000000..7276c70 --- /dev/null +++ b/patch-displays.js @@ -0,0 +1,48 @@ +const fs = require('fs'); + +function patchFile(file) { + let content = fs.readFileSync(file, 'utf8'); + + // Change destructuring to include isCar as isCarMeta, etc. + content = content.replace( + /const { logoSrc, typeLabel, numTrain } = getTrainLogoAndMeta\(horaire\);/g, + "const { logoSrc, typeLabel, numTrain, isCar: metaIsCar, isSubstitution: metaIsSubstitution, brand: metaBrand } = getTrainLogoAndMeta(horaire);" + ); + + // Patch the isCar definition + content = content.replace( + /const isCar = \([\s\S]*?\s*Boolean\(horaire\.is_substitution\) \|\|[\s\S]*?\s*isSubstitution\(horaire\)\n\t\);/, + "const isCar = metaIsCar || Boolean(horaire.is_substitution) || isSubstitution(horaire);\n\tconst isSub = metaIsSubstitution || Boolean(horaire.is_substitution) || isSubstitution(horaire);" + ); + + // If the type column is rendered, we can apply a class based on metaBrand + // Example:
+ // We can add a data-brand={metaBrand} or just use style/class. Let's just modify the typeLabel styling slightly if needed. + // Actually, we also need to append "CAR DE REMPLACEMENT" to status if isSub is true. + + // Let's replace `const status = horaire._status || computeStatusForHoraireAtStop(...)` + // No, let's just append to `showStatus` or similar where the marquee displays the cause. + + // Let's find `
` or similar + content = content.replace( + /
/g, + `
` + ); + + content = content.replace( + /
/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 }; }