PATCH v1.0.1 //

-> Amélioration de la mise en relation des TYPES DE TRAINS PAR CODE DE CIRCULATION (BETA)
This commit is contained in:
AdminGit committed 2026-10-07 01:10:54 +02:00
1 parent d7d4f9cecf
commit c30a0e9002
32 files changed
+2042 -532

No files matched your search

-80
View File
@@ -1,80 +0,0 @@
# 🚅 Afficheurs Ferrovia
Application web de téléaffichage ferroviaire et d'information voyageurs en temps réel (PIDS - Passenger Information Display Systems), reproduisant fidèlement les écrans d'affichage des gares françaises (SNCF).
---
## ✨ Fonctionnalités
- **📡 Données Réelles & Complètes :**
- Horaires théoriques issus de l'archive officielle **GTFS SNCF** (3 507 gares, 41 847 circulations : TGV INOUI, OUIGO, TER, INTERCITÉS, ICE, TGV Lyria).
- Retards, perturbations et suppressions en direct via **GTFS-RT**.
- Détection et affichage des **voies et quais réels** via les flux **SIRI-Lite**.
- **🖥️ Multiples Générations d'Afficheurs :**
- **AFL (Afficheurs Fluides & Lumineux) :** Écrans modernes pour halls et quais (Départs / Arrivées).
- **Classiques :** Écrans d'affichage historiques (Départs, Arrivées, Écrans de Quai avec liste des arrêts desservis).
- **EVA :** Écrans vidéo voyageurs enrichis.
- **🔊 Module d'Annonces Sonores :** Jingles et annonces synthétiques en temps réel.
- **🔍 Recherche Intelligente des Gares :** Reconnaissance instantanée des gares, codes UIC, variantes d'écriture et abréviations (`Valence-TGV`, `Paris-Montparnasse`, `Lyon-Part-Dieu`, etc.).
---
## 📋 Patchnotes / Historique des Versions
Pour consulter le journal détaillé de toutes les versions, consultez le fichier **[CHANGELOG.md](./CHANGELOG.md)**.
<details open>
<summary><h3>🚀 Version 4.0.0 (V4) - <em>Dernière version</em></h3></summary>
- **Intégration GTFS & GTFS-RT Exhaustive :** 3 507 gares et 41 847 circulations réelles intégrées sans données mockées.
- **Quais Réels SIRI-Lite :** Extraction en continu des quais réels avec isolation stricte par gare sélectionnée et par arrêt.
- **Recherche Intelligente :** Algorithme tolérant aux tirets, accents et alias pour tous les noms de gares.
- **Normalisation des Horaires :** Prise en charge des trains de nuit et horaires modulo 24h.
- **Codebase Propre :** Résolution complète de tous les avertissements ESLint (`Compiled successfully`).
</details>
<details>
<summary><h3>🚆 Version 3.0.0 (V3)</h3></summary>
- Nouveaux types d'afficheurs AFL, Classiques et EVA.
- Module d'annonces sonores en temps réel.
- Gestion avancée des trains combinés / multi-tranches et des substitutions par autocar.
</details>
<details>
<summary><h3>🚉 Version 2.0.0 (V2)</h3></summary>
- Écrans dédiés par voie et quai avec liste déroulante des arrêts aval.
- Alternance dynamique des états de circulation et synchronisation automatique.
</details>
<details>
<summary><h3>🏁 Version 1.0.0 (V1)</h3></summary>
- Lancement initial de la plateforme avec horloge temps réel et reproduction de la signalétique ferroviaire.
</details>
---
## 🛠️ Installation & Démarrage
```bash
# Installer les dépendances
npm install
# Lancer le serveur de développement (http://localhost:3000)
npm start
# Compiler pour la production
npm run build
```
---
## 📄 Licence
Projet développé pour la simulation et la restitution d'écrans d'information ferroviaire. Données fournies sous licence ouverte via transport.data.gouv.fr et SNCF Open Data.
+1005
View File
File diff suppressed because it is too large. Load diff
-35
View File
@@ -1,35 +0,0 @@
const fs = require('fs');
const path = require('path');
const files = [
{ p: 'src/pages/afficheurs/afl/AFLDeparts.js', arr: false },
{ p: 'src/pages/afficheurs/afl/AFLArrivees.js', arr: true },
{ p: 'src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js', arr: false },
{ p: 'src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js', arr: true },
{ p: 'src/pages/afficheurs/afl/quais/AFLDepartsQuais.js', arr: false },
{ p: 'src/pages/afficheurs/afl/quais/AFLArriveesQuais.js', arr: true },
];
for (const { p, arr } of files) {
const fullPath = path.join(process.cwd(), p);
if (!fs.existsSync(fullPath)) {
console.log(`File not found: ${fullPath}`);
continue;
}
let content = fs.readFileSync(fullPath, 'utf8');
if (!content.includes('import useAutoAnnonce from')) {
content = content.replace(
"import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';",
"import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';\nimport useAutoAnnonce from '../../../hooks/useAutoAnnonce';"
);
}
if (!content.includes('useAutoAnnonce(')) {
const hookCall = ` useAnnonceSonoreRealtime(resolvedGareId);\n useAutoAnnonce(horaires, currentTime, ${arr});`;
content = content.replace(" useAnnonceSonoreRealtime(resolvedGareId);", hookCall);
}
fs.writeFileSync(fullPath, content, 'utf8');
console.log(`Patched ${p}`);
}
+61
View File
@@ -0,0 +1,61 @@
# Plan d'implémentation : Correction de l'affichage des types de trains (TER Régionaux)
## Problématique analysée
Les données brutes issues des flux GTFS de la SNCF indiquent souvent simplement le type commercial générique `"TER"`, sans préciser la marque régionale (Mobigo, liO, Rémi, Hauts-de-France, etc.).
Actuellement, l'application tente de déduire ces marques via un patch "en dur" dans `src/utils/trainLogoHelper.js` pour Mobigo (`startsWith('89')` ou `startsWith('84')`).
Ce patch est **erroné** :
1. La série `84xxxx` englobe les trains des Hauts-de-France (ex: `841xxx`, `844947` sur Dunkerque/Valenciennes). C'est ce qui cause l'affichage "MOBIGO" en erreur sur l'Image 1.
2. L'Image 2 montre des trains d'Occitanie (876406) affichés comme "Rémi" et des confusions entre tranches. L'absence d'identification stricte basée sur un référentiel fiable entraîne des conflits.
## Contraintes respectées
- **Pas de variables locales / géographiques** : Suppression totale ou non-utilisation des listes de villes associées à des régions (ex: `detectRegionByRouteScore` qui associe 'Dunkerque' à HDF).
- **Utilisation du numéro de circulation de manière fiable** : Nous nous basons **strictement** sur le référentiel d'EspaceRails fourni pour l'immatriculation des TER.
## Actions prévues (Implémentation)
### 1. Refonte du fallback régional dans `trainLogoHelper.js`
Nous allons remplacer la condition codée en dur (et fausse) pour "MOBIGO" par un utilitaire de classification exhaustif basé fidèlement sur les données d'EspaceRails. Cette fonction sera appelée si le train est identifié comme un `TER` générique.
### 2. Plan de numérotation déterministe (Le "Dictionnaire" EspaceRails)
La fonction extraira le numéro (ex: `844947` => préfixe `844`) et appliquera le tableau de correspondance suivant, construit à partir de votre source :
* **Grand Est (Fluo)**
* *Alsace* : 830 à 832, 160
* *Lorraine* : 833 à 837, 161
* *Champagne-Ardenne* : 838 à 840, 162
* **Hauts-de-France**
* *Nord-Pas-de-Calais* : 841 à 846, 163 à 164
* *Picardie* : 847 à 849, 164
* **Normandie (Nomad)**
* *Haute-Normandie* : 850 à 851, 165
* *Basse-Normandie* : 852 à 853, 165
* **Bretagne (BreizhGo)** : 854 à 856, 166
* **Pays de la Loire (Aléop)** : 857 à 859, 166 à 167
* **Centre-Val de Loire (Rémi)** : 860 à 862, 167 à 168
* **Nouvelle-Aquitaine**
* *Poitou-Charentes* : 863 à 864, 168
* *Aquitaine* : 865 à 867, 169
* *Limousin* : 868 à 869, 170
* **Occitanie (liO)**
* *Midi-Pyrénées* : 870 à 872, 171
* *Languedoc-Roussillon* : 876 à 878, 173
* **Auvergne-Rhône-Alpes**
* *Auvergne* : 873 à 875, 172
* *Rhône-Alpes* : 882 à 890, 175 à 179
* **Provence-Alpes-Côte d'Azur (ZOU)** : 879 à 881, 173 à 174
* **Bourgogne-Franche-Comté (Mobigo)**
* *Bourgogne* : 891 à 893, 177 à 178
* *Franche-Comté* : 894 à 895, 178
*(Ce référentiel montre bien que "841" est en Hauts-de-France (NPDC), ce qui corrige l'Img 1, et que "876" est bien Languedoc-Roussillon (liO), ce qui corrige l'Img 2).*
### 3. Application dans `getTrainLogoAndMeta`
Si l'opérateur brut du GTFS est un terme générique (comme `"TER"`), la fonction :
1. Extraira le numéro à 5 ou 6 chiffres.
2. Analysera les 3 premiers chiffres (ex: `844`).
3. Consultera le dictionnaire déterministe EspaceRails.
4. Remplacera le `typeLabel` et associera le bon logo via `logoLookup`.
> Cela garantit la fiabilité demandée, supprime l'usage de variables locales "villes", et solutionne définitivement les bugs d'affichage des Images 1 et 2 en s'appuyant sur les préfixes documentés.
Validez-vous l'intégration de cette logique dans `src/utils/trainLogoHelper.js` ?
-19
View File
@@ -1,19 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabaseUrl = process.env.REACT_APP_SUPABASE_URL;
const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY;
const supabase = createClient(supabaseUrl, supabaseAnonKey);
async function run() {
const { data: gares, error: err1 } = await supabase.from('gares').select('*').ilike('nom', '%Ouges%').limit(1);
console.log("Gares:", gares);
if (err1) console.error(err1);
const { data: horaires, error: err2 } = await supabase.from('horaires').select('*').limit(5);
// find one that stops at Ouges
const { data: horairesOuges, error: err3 } = await supabase.from('horaires').select('*').ilike('gares_desservies', '%Ouges%').limit(2);
console.log("Horaires Ouges:", JSON.stringify(horairesOuges, null, 2));
}
run();
-11
View File
@@ -1,11 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data, error } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies, quai_reel').or('gare_depart_nom.ilike.%Ouges%,gare_arrivee_nom.ilike.%Ouges%').limit(5);
console.log("Horaires Ouges:", JSON.stringify(data, null, 2));
}
run();
-13
View File
@@ -1,13 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data, error } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies, quai_reel').limit(10);
// find any train with Ouges
const filtered = data.filter(h => JSON.stringify(h).includes("Ouges"));
console.log("Horaires Ouges filtered:", JSON.stringify(filtered, null, 2));
}
run();
-12
View File
@@ -1,12 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data, error } = await supabase.from('horaires').select('*').limit(1);
console.log("Horaires schema:", Object.keys(data[0] || {}));
if (error) console.error("Error:", error);
}
run();
-19
View File
@@ -1,19 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data, error } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies').or('gare_depart_nom.ilike.%Ouges%,gare_arrivee_nom.ilike.%Ouges%');
if (!data || data.length === 0) {
// try to fetch all and filter in JS
const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies').limit(200);
const filtered = allData.filter(h => JSON.stringify(h).includes("Ouges"));
console.log("Horaires Ouges (JS filter):", JSON.stringify(filtered.slice(0, 2), null, 2));
} else {
console.log("Horaires Ouges:", JSON.stringify(data.slice(0, 2), null, 2));
}
}
run();
-9
View File
@@ -1,9 +0,0 @@
require('dotenv').config();
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data, error } = await supabase.from('annonces_diffusions').select('*').order('created_at', { ascending: false }).limit(1);
if (error) console.error(error);
else console.log(JSON.stringify(data[0], null, 2));
}
run();
-9
View File
@@ -1,9 +0,0 @@
require('dotenv').config();
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data, error } = await supabase.from('annonces').select('*').limit(2);
if (error) console.error(error);
else console.log(JSON.stringify(data, null, 2));
}
run();
-9
View File
@@ -1,9 +0,0 @@
require('dotenv').config();
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data, error } = await supabase.from('annonces_diffusions').select('*').limit(1);
if (error) console.error(error);
else console.log(Object.keys(data[0]));
}
run();
-9
View File
@@ -1,9 +0,0 @@
require('dotenv').config();
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data, error } = await supabase.from('horaires').select('*').limit(1);
if (error) console.error(error);
else console.log(JSON.stringify(data[0], null, 2));
}
run();
-12
View File
@@ -1,12 +0,0 @@
require('dotenv').config();
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data, error } = await supabase.from('annonces_diffusions').select(`
*,
annonce:annonces_enregistrees(variables_values)
`).order('created_at', { ascending: false }).limit(1);
if (error) console.error(error);
else console.log(JSON.stringify(data[0], null, 2));
}
run();
+935
View File
@@ -0,0 +1,935 @@
/**
* Moteur de classification et de corrélation des types de trains et opérateurs ferroviaires
* ferrovia-infogare - Compatible afficheurs classiques (Hall & Quai)
*
* Hiérarchie de décision :
* 1. Codes d'exploitation GTFS officiels (trip_id SNCF Open Data)
* 2. Détection des services spécifiques (Lyria, Ouigo Classique, Intercités de Nuit, etc.)
* 3. Plan de Numérotation officiel SNCF (séries régionales TER hermétiques par blocs de numéros)
* 4. Détection géographique par score d'arrêts et de terminus (sans faux-positifs)
* 5. Re-routage vers TER générique ou SNCF Voyageurs en dernier recours
*/
// Normalisation des chaînes (minuscules, sans accents, espaces normalisés)
export const normalize = (str) => {
if (!str) return '';
return String(str)
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
};
// Extraction des chiffres d'un numéro de train (sans zéros initiaux)
export const extractTrainDigits = (num) => {
if (!num) return '';
const digits = String(num).replace(/\D/g, '');
return digits.replace(/^0+/, '') || digits;
};
// Services routiers par autocar (3xxxx / 3xxxxx)
export const isCarTrainNumber = (num) => {
const d = String(num || '').replace(/\D/g, '');
return (d.length === 5 && d.startsWith('3')) || (d.length === 6 && d.startsWith('3'));
};
/**
* Référentiel des marques, types de trains et logos (monochrome Hall & couleur Quai)
*/
export const BRANDS = {
INOUi: {
id: 'inoui',
typeLabel: 'TGV inOUI',
quaiLabel: 'TGV INOUI',
operator: 'SNCF Voyageurs',
category: 'TGV',
isTgv: true,
hallLogo: '/type/logo-inoui.svg',
quaiLogo: '/type/quais/logo-inoui-color.svg',
showVoyageursText: false
},
TGV_GENERIC: {
id: 'tgv',
typeLabel: 'TGV',
quaiLabel: 'TGV',
operator: 'SNCF Voyageurs',
category: 'TGV',
isTgv: true,
hallLogo: '/type/logo-tgv.svg',
quaiLogo: '/type/quais/logo-tgv-color.svg',
showVoyageursText: false
},
LYRIA: {
id: 'lyria',
typeLabel: 'TGV Lyria',
quaiLabel: 'TGV Lyria',
operator: 'Lyria SAS',
category: 'TGV',
isTgv: true,
hallLogo: '/type/logo-lyria.svg',
quaiLogo: '/type/quais/logo-lyria-color.svg',
showVoyageursText: false
},
OUIGO_CLASSIQUE: {
id: 'ouigo-classique',
typeLabel: 'OUIGO Train Classique',
quaiLabel: 'OUIGO Classique',
operator: 'SNCF Voyageurs',
category: 'GRANDES_LIGNES',
isTgv: false,
hallLogo: '/type/logo-ouigo-classique.svg',
quaiLogo: '/type/quais/logo-ouigo-classique-color.svg',
showVoyageursText: false
},
OUIGO_GV: {
id: 'ouigo',
typeLabel: 'OUIGO',
quaiLabel: 'OUIGO',
operator: 'SNCF Voyageurs',
category: 'TGV',
isTgv: true,
hallLogo: '/type/logo-ouigo.svg',
quaiLogo: '/type/quais/logo-ouigo-color.svg',
showVoyageursText: false
},
INTERCITES_NUIT: {
id: 'intercites-nuit',
typeLabel: 'INTERCITÉS DE NUIT',
quaiLabel: 'Intercités de Nuit',
operator: 'SNCF Voyageurs',
category: 'GRANDES_LIGNES',
isTgv: false,
hallLogo: '/type/logo-lunea.svg',
quaiLogo: '/type/quais/logo-lunea-color.svg',
showVoyageursText: false
},
INTERCITES: {
id: 'intercites',
typeLabel: 'INTERCITÉS',
quaiLabel: 'INTERCITÉS',
operator: 'SNCF Voyageurs',
category: 'GRANDES_LIGNES',
isTgv: false,
hallLogo: '/type/logo-intercites.svg',
quaiLogo: '/type/quais/logo-intercite-color.svg',
showVoyageursText: false
},
EUROSTAR: {
id: 'eurostar',
typeLabel: 'EUROSTAR',
quaiLabel: 'Eurostar',
operator: 'Eurostar Group',
category: 'INTERNATIONAL',
isTgv: true,
hallLogo: '/type/logo-eurostar.svg',
quaiLogo: '/type/quais/logo-eurostar-color.svg',
showVoyageursText: false
},
THALYS: {
id: 'thalys',
typeLabel: 'THALYS',
quaiLabel: 'Thalys',
operator: 'Eurostar Group',
category: 'INTERNATIONAL',
isTgv: true,
hallLogo: '/type/logo-thalys.svg',
quaiLogo: '/type/quais/logo-thalys-color.svg',
showVoyageursText: false
},
ICE: {
id: 'ice',
typeLabel: 'ICE',
quaiLabel: 'ICE',
operator: 'DB / SNCF',
category: 'INTERNATIONAL',
isTgv: true,
hallLogo: '/type/logo-ice.svg',
quaiLogo: '/type/quais/logo-ice-color.svg',
showVoyageursText: false
},
DB: {
id: 'db',
typeLabel: 'DB',
quaiLabel: 'DB',
operator: 'Deutsche Bahn',
category: 'INTERNATIONAL',
isTgv: false,
hallLogo: '/type/logo-db.svg',
quaiLogo: '/type/quais/logo-db-color.svg',
showVoyageursText: false
},
FRECCIAROSSA: {
id: 'frecciarossa',
typeLabel: 'FRECCIAROSSA',
quaiLabel: 'Frecciarossa',
operator: 'Trenitalia France',
category: 'INTERNATIONAL',
isTgv: true,
hallLogo: '/type/logo-frecciarossa.svg',
quaiLogo: '/type/quais/logo-frecciarossa-color.svg',
showVoyageursText: false
},
RENFE_AVE: {
id: 'renfe-ave',
typeLabel: 'Renfe AVE',
quaiLabel: 'Renfe AVE',
operator: 'Renfe',
category: 'INTERNATIONAL',
isTgv: true,
hallLogo: '/type/logo-renfeave.svg',
quaiLogo: '/type/quais/logo-renfeave-color.svg',
showVoyageursText: false
},
AUTOCAR: {
id: 'autocar',
typeLabel: 'AUTOCAR',
quaiLabel: 'Autocar',
operator: 'SNCF Voyageurs',
category: 'AUTOCAR',
isTgv: false,
hallLogo: '/type/track-car.svg',
quaiLogo: '/type/track-car.svg',
showVoyageursText: false
},
TRAMTRAIN: {
id: 'tramtrain',
typeLabel: 'Tram-train',
quaiLabel: 'Tram-train',
operator: 'TER',
category: 'TER',
isTgv: false,
hallLogo: '/type/logo-tramtrain.svg',
quaiLogo: '/type/quais/logo-tramtrain-color.svg',
showVoyageursText: false
},
MOBIGO: {
id: 'mobigo',
typeLabel: 'MOBIGO',
quaiLabel: 'Mobigo',
operator: 'Région Bourgogne-Franche-Comté',
category: 'TER',
isTgv: false,
hallLogo: '/type/logo-mobigo.svg',
quaiLogo: '/type/quais/logo-mobigo-color.svg',
showVoyageursText: false
},
ZOU: {
id: 'zou',
typeLabel: 'ZOU !',
quaiLabel: 'ZOU!',
operator: 'Région Sud / PACA',
category: 'TER',
isTgv: false,
hallLogo: '/type/logo-zou.svg',
quaiLogo: '/type/quais/logo-zou-color.svg',
showVoyageursText: false
},
FLUO: {
id: 'fluo',
typeLabel: 'FLUO',
quaiLabel: 'TER Fluo',
operator: 'Région Grand Est',
category: 'TER',
isTgv: false,
hallLogo: '/type/logo-fluo.svg',
quaiLogo: '/type/quais/logo-fluo-color.svg',
showVoyageursText: false
},
ALEOP: {
id: 'aleop',
typeLabel: 'ALÉOP',
quaiLabel: 'Aléop',
operator: 'Région Pays de la Loire',
category: 'TER',
isTgv: false,
hallLogo: '/type/logo-aleop.svg',
quaiLogo: '/type/quais/logo-aleop-color.svg',
showVoyageursText: false
},
BREIZHGO: {
id: 'breizhgo',
typeLabel: 'BREIZHGO',
quaiLabel: 'BreizhGo',
operator: 'Région Bretagne',
category: 'TER',
isTgv: false,
hallLogo: '/type/logo-breizhgo.svg',
quaiLogo: '/type/quais/logo-breizhgo-color.svg',
showVoyageursText: false
},
NOMAD: {
id: 'nomad',
typeLabel: 'NOMAD',
quaiLabel: 'Nomad',
operator: 'Région Normandie',
category: 'TER',
isTgv: false,
hallLogo: '/type/logo-nomad.svg',
quaiLogo: '/type/quais/logo-nomad-color.svg',
showVoyageursText: false
},
REMI: {
id: 'remi',
typeLabel: 'RÉMI',
quaiLabel: 'Rémi',
operator: 'Région Centre-Val de Loire',
category: 'TER',
isTgv: false,
hallLogo: '/type/logo-remi.svg',
quaiLogo: '/type/quais/logo-remi-color.svg',
showVoyageursText: false
},
LIO: {
id: 'lio',
typeLabel: 'LIÒ',
quaiLabel: 'liO',
operator: 'Région Occitanie',
category: 'TER',
isTgv: false,
hallLogo: '/type/logo-lio.svg',
quaiLogo: '/type/quais/logo-lio-color.svg',
showVoyageursText: false
},
HAUTS_DE_FRANCE: {
id: 'hautsdefrance',
typeLabel: 'HAUTS-DE-FRANCE',
quaiLabel: 'Hauts-de-France',
operator: 'Région Hauts-de-France',
category: 'TER',
isTgv: false,
hallLogo: '/type/logo-hautsdefrance.svg',
quaiLogo: '/type/quais/logo-hautsdefrance-color.svg',
showVoyageursText: false
},
AUVERGNE_RHONE_ALPES: {
id: 'auvergerhonealpes',
typeLabel: 'AUVERGNE-RHÔNE-ALPES',
quaiLabel: 'Auvergne-Rhône-Alpes',
operator: 'Région Auvergne-Rhône-Alpes',
category: 'TER',
isTgv: false,
hallLogo: '/type/logo-auvergerhonealpes.svg',
quaiLogo: '/type/quais/logo-auvergerhonealpes-color.svg',
showVoyageursText: false
},
NOUVELLE_AQUITAINE: {
id: 'nouvelleaquitaine',
typeLabel: 'NOUVELLE-AQUITAINE',
quaiLabel: 'Nouvelle-Aquitaine',
operator: 'Région Nouvelle-Aquitaine',
category: 'TER',
isTgv: false,
hallLogo: '/type/logo-teraquitaine.svg',
quaiLogo: '/type/quais/logo-nouvelleaquitaine-color.svg',
showVoyageursText: false
},
TER_GENERIC: {
id: 'ter',
typeLabel: 'TER',
quaiLabel: 'TER',
operator: 'SNCF Voyageurs',
category: 'TER',
isTgv: false,
hallLogo: '/type/logo-ter.svg',
quaiLogo: '/type/quais/logo-ter-color.svg',
showVoyageursText: false
},
SBB: {
id: 'sbb',
typeLabel: 'SBB',
quaiLabel: 'SBB',
operator: 'CFF / SBB',
category: 'INTERNATIONAL',
isTgv: false,
hallLogo: '/type/logo-sbb.svg',
quaiLogo: '/type/quais/logo-sbb-color.svg',
showVoyageursText: false
},
SNCB: {
id: 'sncb',
typeLabel: 'SNCB',
quaiLabel: 'SNCB',
operator: 'SNCB / NMBS',
category: 'INTERNATIONAL',
isTgv: false,
hallLogo: '/type/logo-sncb.svg',
quaiLogo: '/type/quais/logo-sncb-color.svg',
showVoyageursText: false
},
CFL: {
id: 'cfl',
typeLabel: 'CFL',
quaiLabel: 'CFL',
operator: 'CFL',
category: 'INTERNATIONAL',
isTgv: false,
hallLogo: '/type/logo-cfl.svg',
quaiLogo: '/type/quais/logo-cfl-color.svg',
showVoyageursText: false
},
SNCF_GENERIC: {
id: 'sncf',
typeLabel: 'SNCF',
quaiLabel: 'SNCF',
operator: 'SNCF Voyageurs',
category: 'SNCF',
isTgv: false,
hallLogo: '/type/logo-sncf.svg',
quaiLogo: '/type/quais/logo-sncf-color.svg',
showVoyageursText: true
}
};
/**
* Plan de Numérotation officiel SNCF (Séries régionales TER)
* Chaque autorité organisatrice régionale dispose de tranches numériques réservées.
*/
function getRegionByTrainNumber(numStr) {
const clean = String(numStr || '').replace(/\D/g, '');
if (!clean || clean.length < 3) return null;
// 1. Occitanie (liO) : 869xxx, 870xxx, 871xxx, 872xxx, 876xxx, 877xxx (et 5 chiffres : 869xx, 870xx, 876xx, 877xx)
if (
clean.startsWith('869') ||
clean.startsWith('870') ||
clean.startsWith('871') ||
clean.startsWith('872') ||
clean.startsWith('876') ||
clean.startsWith('877')
) {
return BRANDS.LIO;
}
// 2. Centre-Val de Loire (Rémi) : 860xxx, 861xxx, 862xxx, 863xxx (et 860xx à 863xx)
if (
clean.startsWith('860') ||
clean.startsWith('861') ||
clean.startsWith('862') ||
clean.startsWith('863')
) {
return BRANDS.REMI;
}
// 3. Nouvelle-Aquitaine : 864xxx, 865xxx, 866xxx, 867xxx, 868xxx (et 864xx à 868xx)
if (
clean.startsWith('864') ||
clean.startsWith('865') ||
clean.startsWith('866') ||
clean.startsWith('867') ||
clean.startsWith('868')
) {
return BRANDS.NOUVELLE_AQUITAINE;
}
// 4. Grand Est (Fluo) : 83xxxx (et 83xxx)
if (clean.startsWith('83')) {
return BRANDS.FLUO;
}
// 5. Bourgogne-Franche-Comté (Mobigo) : 89xxxx, 840xxx, 841xxx (et 89xxx, 840xx, 841xx)
if (
clean.startsWith('89') ||
clean.startsWith('840') ||
clean.startsWith('841')
) {
return BRANDS.MOBIGO;
}
// 6. Hauts-de-France : 842xxx à 849xxx (et 842xx à 849xx)
if (['842', '843', '844', '845', '846', '847', '848', '849'].some(p => clean.startsWith(p))) {
return BRANDS.HAUTS_DE_FRANCE;
}
// 7. Normandie (Nomad) : 852xxx, 853xxx (et 852xx, 853xx)
if (clean.startsWith('852') || clean.startsWith('853')) {
return BRANDS.NOMAD;
}
// 8. Bretagne (BreizhGo) : 854xxx, 855xxx, 856xxx (et 854xx à 856xx)
if (clean.startsWith('854') || clean.startsWith('855') || clean.startsWith('856')) {
return BRANDS.BREIZHGO;
}
// 9. Pays de la Loire (Aléop) : 857xxx, 858xxx, 859xxx (et 857xx à 859xx)
if (clean.startsWith('857') || clean.startsWith('858') || clean.startsWith('859')) {
return BRANDS.ALEOP;
}
// 10. Auvergne-Rhône-Alpes : 873xxx à 875xxx, 883xxx, 884xxx, 889xxx
if (['873', '874', '875', '883', '884', '889'].some(p => clean.startsWith(p))) {
return BRANDS.AUVERGNE_RHONE_ALPES;
}
// 11. PACA / Sud (Zou !) : 880xxx, 881xxx, 882xxx, 885xxx, 886xxx, 887xxx, 888xxx
if (['880', '881', '882', '885', '886', '887', '888'].some(p => clean.startsWith(p))) {
return BRANDS.ZOU;
}
return null;
}
/**
* Gares de référence par région ferroviaire pour analyse de parcours
* (Seuls des noms de villes ou gares réelles sans mots génériques comme 'centre' ou 'est')
*/
const REGIONAL_STATIONS = {
lio: [
'perpignan', 'narbonne', 'toulouse', 'nimes', 'beziers', 'sete', 'montpellier',
'cerbere', 'portbou', 'villefranche', 'carcassonne', 'agde', 'elne', 'argeles',
'collioure', 'port vendres', 'banyuls', 'rodez', 'albi', 'tarbes', 'montauban',
'castres', 'lourdes', 'mende', 'foix', 'latour de carol', 'ax les thermes',
'castelnaudary', 'frontignan', 'port la nouvelle', 'leucate', 'salses', 'rivesaltes'
],
remi: [
'orleans', 'tours', 'bourges', 'blois', 'chartres', 'chateauroux', 'vierzon',
'vendome', 'saint pierre des corps', 'amboise', 'nogent le rotrou', 'salbris',
'chinon', 'loches', 'issoudun', 'romorantin', 'montargis', 'gien'
],
mobigo: [
'dijon', 'besancon', 'belfort', 'auxerre', 'nevers', 'chalon sur saone',
'montbeliard', 'macon', 'sens', 'dole', 'creusot', 'paray le monial',
'beaune', 'lons le saunier', 'pontarlier', 'vesoul'
],
zou: [
'marseille', 'nice', 'toulon', 'cannes', 'antibes', 'avignon', 'grasse',
'aubagne', 'miramas', 'arles', 'hyeres', 'menton', 'cagnes sur mer',
'frejus', 'saint raphael', 'la ciotat', 'gap', 'briancon', 'manosque'
],
fluo: [
'strasbourg', 'nancy', 'metz', 'mulhouse', 'colmar', 'reims', 'troyes',
'epinal', 'thionville', 'charleville', 'saint die', 'haguenau',
'sarreguemines', 'bar le duc', 'luneville', 'sedan', 'forbach', 'selestat'
],
aleop: [
'nantes', 'angers', 'le mans', 'saint nazaire', 'cholet', 'la roche sur yon',
'saumur', 'laval', 'les sables d olonne', 'ancenis', 'clisson', 'chateaubriant'
],
breizhgo: [
'rennes', 'brest', 'quimper', 'saint malo', 'vannes', 'lorient',
'saint brieuc', 'morlaix', 'lannion', 'vitre', 'redon', 'guingamp'
],
nomad: [
'rouen', 'caen', 'le havre', 'cherbourg', 'evreux', 'trouville deauville',
'dieppe', 'lisieux', 'granville', 'alencon', 'bernay', 'bayeux'
],
hautsdefrance: [
'lille', 'amiens', 'calais', 'dunkerque', 'valenciennes', 'arras',
'boulogne sur mer', 'douai', 'creil', 'saint quentin', 'lens', 'bethune',
'compiegne', 'cambrai', 'tourcoing', 'roubaix'
],
auvergerhonealpes: [
'lyon', 'grenoble', 'chambery', 'annecy', 'valence', 'clermont ferrand',
'saint etienne', 'bourg en bresse', 'roanne', 'vichy', 'aix les bains',
'annemasse', 'amberieu', 'riom', 'moulins'
],
nouvelleaquitaine: [
'bordeaux', 'limoges', 'poitiers', 'la rochelle', 'bayonne', 'pau',
'angouleme', 'perigueux', 'agen', 'niort', 'brive la gaillarde', 'dax',
'hendaye', 'saintes', 'arcachon'
]
};
/**
* Calcule la région la plus probable selon les arrêts desservis et le terminus
*/
function detectRegionByRouteScore(routeText) {
if (!routeText) return null;
const normalized = ` ${normalize(routeText)} `;
let bestRegion = null;
let maxScore = 0;
for (const [regionKey, stations] of Object.entries(REGIONAL_STATIONS)) {
let score = 0;
for (const station of stations) {
const stationNorm = ` ${normalize(station)} `;
if (normalized.includes(stationNorm)) {
score++;
}
}
if (score > maxScore) {
maxScore = score;
bestRegion = regionKey;
}
}
if (maxScore > 0 && bestRegion) {
switch (bestRegion) {
case 'lio': return BRANDS.LIO;
case 'remi': return BRANDS.REMI;
case 'mobigo': return BRANDS.MOBIGO;
case 'zou': return BRANDS.ZOU;
case 'fluo': return BRANDS.FLUO;
case 'aleop': return BRANDS.ALEOP;
case 'breizhgo': return BRANDS.BREIZHGO;
case 'nomad': return BRANDS.NOMAD;
case 'hautsdefrance': return BRANDS.HAUTS_DE_FRANCE;
case 'auvergerhonealpes': return BRANDS.AUVERGNE_RHONE_ALPES;
case 'nouvelleaquitaine': return BRANDS.NOUVELLE_AQUITAINE;
default: return null;
}
}
return null;
}
/**
* Fonction centrale de classification d'un train à partir de ses données SNCF
* @param {Object|string} trainOrType - Objet horaire ou type brut
* @param {string} [operatorParam] - Opérateur ou transporteur optionnel
* @param {string} [numParam] - Numéro de train optionnel
* @returns {Object} Résultat complet de classification
*/
export function classifyTrain(trainOrType, operatorParam = '', numParam = '') {
let horaire = null;
let rawType = '';
let opRaw = operatorParam || '';
let numRaw = numParam || '';
if (trainOrType && typeof trainOrType === 'object') {
horaire = trainOrType;
rawType = horaire.type_train || horaire.type || horaire.train_type || horaire.commercial_mode || horaire.mode || '';
opRaw = horaire.operator || horaire.op || horaire.transporteur || horaire.carrier || opRaw;
numRaw = horaire.numero_train || horaire.numero || horaire.number || horaire.num || horaire.trip_short_name || numRaw;
} else {
rawType = String(trainOrType || '');
}
const numTrain = extractTrainDigits(numRaw);
const numInt = parseInt(numTrain, 10) || 0;
const tripId = horaire ? String(horaire.trip_id || '') : '';
const isSubstitution = Boolean(horaire && (horaire.is_substitution || horaire.is_sub));
const origName = horaire ? (horaire.gare_depart_nom || horaire.orig || horaire.origin || '') : '';
const destName = horaire ? (horaire.gare_arrivee_nom || horaire.dest || horaire.destination || '') : '';
const stopsText = Array.isArray(horaire?.stops) ? horaire.stops.map(s => (s.nom || s.gare_nom || s.name || '')).join(' ') : '';
const fullRouteText = `${origName} ${destName} ${stopsText}`;
const routeNormalized = normalize(fullRouteText);
const typeNorm = normalize(rawType);
const opNorm = normalize(opRaw);
const combined = `${typeNorm} ${opNorm}`.trim();
// Helper pour créer le résultat enrichi
const buildResult = (brand, customProps = {}) => ({
...brand,
numTrain,
rawType,
rawOperator: opRaw,
...customProps
});
// =========================================================================
// 1. SERVICES AUTOCAR / CAR TER / BUS DE SUBSTITUTION
// =========================================================================
if (
isSubstitution ||
isCarTrainNumber(numTrain) ||
combined.includes('autocar') ||
combined.includes('carter') ||
combined.includes('substitution') ||
combined === 'car' ||
combined === 'bus'
) {
return buildResult(BRANDS.AUTOCAR);
}
// =========================================================================
// 2. TGV LYRIA (Paris - Suisse : Genève, Lausanne, Zurich, Bâle)
// =========================================================================
if (
combined.includes('lyria') ||
tripId.includes(':LYR:FR') ||
(numInt >= 9200 && numInt <= 9299) ||
(numInt >= 9750 && numInt <= 9799)
) {
return buildResult(BRANDS.LYRIA);
}
// =========================================================================
// 3. OUIGO TRAIN CLASSIQUE (4000-4399 ou 8040-8099, Paris Austerlitz/Bercy)
// =========================================================================
const isOuigoClassique = (
combined.includes('ouigoclassique') ||
combined.includes('trainclassique') ||
combined.includes('classique') ||
(combined.includes('ouigo') && ((numInt >= 4000 && numInt <= 4399) || (numInt >= 8040 && numInt <= 8099))) ||
(tripId.includes(':OGO:FR') && ((numInt >= 4000 && numInt <= 4399) || (numInt >= 8040 && numInt <= 8099)))
);
if (isOuigoClassique) {
return buildResult(BRANDS.OUIGO_CLASSIQUE);
}
// =========================================================================
// 4. OUIGO (GRANDE VITESSE)
// =========================================================================
if (
combined.includes('ouigo') ||
tripId.includes(':OGO:FR') ||
(numInt >= 7600 && numInt <= 7999)
) {
return buildResult(BRANDS.OUIGO_GV);
}
// =========================================================================
// 5. INTERCITÉS DE NUIT (Lunéa)
// =========================================================================
if (
combined.includes('denuit') ||
combined.includes('nuit') ||
combined.includes('lunea') ||
tripId.includes(':ICN:FR') ||
(numInt >= 3690 && numInt <= 3799) ||
(numInt >= 3950 && numInt <= 3999) ||
(numInt >= 5700 && numInt <= 5799)
) {
return buildResult(BRANDS.INTERCITES_NUIT);
}
// =========================================================================
// 6. INTERCITÉS (Trains d'Équilibre du Territoire de jour)
// =========================================================================
if (
combined.includes('intercites') ||
combined === 'ic' ||
combined.startsWith('ic ') ||
tripId.includes(':IC:FR') ||
(numInt >= 3000 && numInt <= 3949) ||
(numInt >= 4400 && numInt <= 4999)
) {
return buildResult(BRANDS.INTERCITES);
}
// =========================================================================
// 7. EUROSTAR & THALYS
// =========================================================================
const isEurostarOrThalys = (
combined.includes('eurostar') ||
combined.includes('thalys') ||
(tripId.includes(':TRN:FR') && (routeNormalized.includes('bruxelles') || routeNormalized.includes('london') || routeNormalized.includes('amsterdam') || routeNormalized.includes('koln'))) ||
((numInt >= 60 && numInt <= 99) && (routeNormalized.includes('bruxelles') || routeNormalized.includes('nord'))) ||
(numInt >= 9000 && numInt <= 9499 && (routeNormalized.includes('bruxelles') || routeNormalized.includes('london') || routeNormalized.includes('amsterdam')))
);
if (isEurostarOrThalys) {
if (combined.includes('thalys')) {
return buildResult(BRANDS.THALYS);
}
return buildResult(BRANDS.EUROSTAR);
}
// =========================================================================
// 8. FRECCIAROSSA / TRENITALIA (Paris Lyon - Lyon - Milan)
// =========================================================================
if (
combined.includes('frecciarossa') ||
combined.includes('freccia') ||
combined.includes('trenitalia') ||
(numInt >= 6640 && numInt <= 6670)
) {
return buildResult(BRANDS.FRECCIAROSSA);
}
// =========================================================================
// 9. RENFE AVE (Marseille / Lyon - Madrid / Barcelone)
// =========================================================================
if (
combined.includes('renfe') ||
combined.includes('ave') ||
[9724, 9725, 9731, 9732].includes(numInt)
) {
return buildResult(BRANDS.RENFE_AVE);
}
// =========================================================================
// 10. ICE / DB (Paris Est - Allemagne)
// =========================================================================
if (
combined.includes('ice') ||
combined === 'db' ||
tripId.includes(':ICE:FR') ||
(numInt >= 9550 && numInt <= 9599)
) {
return buildResult(BRANDS.ICE);
}
// =========================================================================
// 11. TGV INOUI & TGV GÉNÉRIQUE
// =========================================================================
if (
combined.includes('inoui') ||
combined.includes('tgvinoui') ||
tripId.includes(':OUI:FR') ||
combined.includes('tgv')
) {
return buildResult(BRANDS.INOUi);
}
// =========================================================================
// 12. TRAM-TRAIN
// =========================================================================
if (
combined.includes('tramtrain') ||
combined.includes('tram-train') ||
tripId.includes(':TT:FR')
) {
return buildResult(BRANDS.TRAMTRAIN);
}
// =========================================================================
// 13. TERGV
// =========================================================================
if (combined.includes('tergv') || combined.includes('ter-gv')) {
return buildResult(BRANDS.HAUTS_DE_FRANCE, { typeLabel: 'TERGV', quaiLabel: 'TERGV' });
}
// =========================================================================
// 14. OPÉRATEURS SUISSE, BELGE, LUXEMBOURGEOIS
// =========================================================================
if (combined.includes('sbb') || combined.includes('cff') || combined.includes('ffs')) {
return buildResult(BRANDS.SBB);
}
if (combined.includes('sncb') || combined.includes('nmbs')) {
return buildResult(BRANDS.SNCB);
}
if (combined.includes('cfl')) {
return buildResult(BRANDS.CFL);
}
// =========================================================================
// 15. TER RÉGIONAUX - DÉTECTION ROBUSTE PAR PALIER
// =========================================================================
// Palier A : Mots-clés régionaux explicites dans l'opérateur ou type
if (combined.includes('lio') || combined.includes('occitanie') || combined.includes('languedoc') || combined.includes('midipyrenees')) {
return buildResult(BRANDS.LIO);
}
if (combined.includes('mobigo') || combined.includes('bourgogne') || combined.includes('franchecomte')) {
return buildResult(BRANDS.MOBIGO);
}
if (combined.includes('zou') || combined.includes('paca') || combined.includes('ler')) {
return buildResult(BRANDS.ZOU);
}
if (combined.includes('fluo') || combined.includes('grandest') || combined.includes('alsace') || combined.includes('lorraine') || combined.includes('metrolor')) {
return buildResult(BRANDS.FLUO);
}
if (combined.includes('aleop') || combined.includes('paysdelaloire')) {
return buildResult(BRANDS.ALEOP);
}
if (combined.includes('breizh') || combined.includes('bretagne')) {
return buildResult(BRANDS.BREIZHGO);
}
if (combined.includes('nomad') || combined.includes('normandie')) {
return buildResult(BRANDS.NOMAD);
}
if (combined.includes('remi') || combined.includes('centrevaldeloire')) {
return buildResult(BRANDS.REMI);
}
if (combined.includes('hautsdefrance') || combined.includes('nordpasdecalais') || combined.includes('picardie')) {
return buildResult(BRANDS.HAUTS_DE_FRANCE);
}
if (combined.includes('auvergerhonealpes') || combined.includes('rhonealpes') || combined.includes('auvergne')) {
return buildResult(BRANDS.AUVERGNE_RHONE_ALPES);
}
if (combined.includes('nouvelleaquitaine') || combined.includes('aquitaine') || combined.includes('poitoucharentes') || combined.includes('limousin')) {
return buildResult(BRANDS.NOUVELLE_AQUITAINE);
}
// Palier B : Plan de numérotation SNCF (Strict & Déterministe)
const regionByNum = getRegionByTrainNumber(numTrain);
if (regionByNum) {
return buildResult(regionByNum);
}
// Palier C : Analyse géographique par score d'arrêts (si train hors numérotation standard)
const regionByRoute = detectRegionByRouteScore(fullRouteText);
if (regionByRoute) {
return buildResult(regionByRoute);
}
// =========================================================================
// 16. TER STANDARD (Fallback)
// =========================================================================
if (combined.includes('ter')) {
return buildResult(BRANDS.TER_GENERIC);
}
// =========================================================================
// 17. SNCF VOYAGEURS (Fallback par défaut)
// =========================================================================
return buildResult(BRANDS.SNCF_GENERIC, {
typeLabel: rawType || 'SNCF',
quaiLabel: rawType || 'SNCF'
});
}
/**
* Interface pour les afficheurs de Quai classiques (ClassiquesDepartsQuais & ClassiquesArriveesQuais)
* @param {Object|string} trainTypeOrHoraire
* @param {string} [operator]
* @param {boolean} [isDeparture=true]
* @param {string} [numTrainOverride]
* @returns {{ logo: string, label: string, showVoyageursText: boolean, isTgv: boolean, classification: Object }}
*/
export function getTrainBrand(trainTypeOrHoraire = '', operator = '', isDeparture = true, numTrainOverride = '') {
const c = classifyTrain(trainTypeOrHoraire, operator, numTrainOverride);
// Pour les trains sans marque spécifique :
if (c.id === 'sncf' || c.id === 'ter') {
if (isDeparture) {
return {
logo: BRANDS.SNCF_GENERIC.quaiLogo,
label: c.quaiLabel || 'SNCF',
showVoyageursText: true,
isTgv: Boolean(c.isTgv),
classification: c
};
}
return {
logo: BRANDS.TER_GENERIC.quaiLogo,
label: c.quaiLabel || 'TER',
showVoyageursText: false,
isTgv: Boolean(c.isTgv),
classification: c
};
}
return {
logo: c.quaiLogo,
label: c.quaiLabel,
showVoyageursText: Boolean(c.showVoyageursText),
isTgv: Boolean(c.isTgv),
classification: c
};
}
/**
* Interface pour les afficheurs Hall classiques (ClassiquesDeparts & ClassiquesArrivees)
* @param {Object} horaire
* @returns {{ logoSrc: string, typeLabel: string, numTrain: string, isTgv: boolean, brand: Object, classification: Object }}
*/
export function getTrainLogoAndMeta(horaire) {
if (!horaire) {
return {
logoSrc: BRANDS.TER_GENERIC.hallLogo,
typeLabel: 'TER',
numTrain: '',
isTgv: false,
brand: getTrainBrand(),
classification: BRANDS.TER_GENERIC
};
}
const c = classifyTrain(horaire);
const brand = getTrainBrand(horaire, '', true);
return {
logoSrc: c.hallLogo,
typeLabel: c.typeLabel,
numTrain: c.numTrain,
isTgv: Boolean(c.isTgv),
brand,
classification: c
};
}
+41 -7
View File
@@ -180,7 +180,7 @@ export function getTrainLogoAndMeta(horaire) {
} else if (opNorm.includes('breizhgo')) {
typeLabel = 'BREIZHGO';
} else if (opNorm.includes('hautsdefrance')) {
typeLabel = 'HAUTS-DE-FRANCE';
typeLabel = 'TER HDF';
} else if (opNorm.includes('lio')) {
typeLabel = 'LIÒ';
} else if (opNorm.includes('zou')) {
@@ -199,14 +199,48 @@ export function getTrainLogoAndMeta(horaire) {
typeLabel = 'TER';
}
// Spécificité régionale Bourgogne-Franche-Comté : TER commençant par 89xxxx ou 84xxxx -> MOBIGO
if (typeLabel === 'TER' && (numTrain.startsWith('89') || numTrain.startsWith('84'))) {
typeLabel = 'MOBIGO';
if (!matchedLogo || matchedLogo.file === 'logo-ter.svg' || matchedLogo.file === 'logo-sncf.svg') {
matchedLogo = logoLookup.get('mobigo') || matchedLogo;
// 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;
if (cleanNum.length >= 4) {
const p3 = parseInt(cleanNum.substring(0, 3), 10);
const p4 = parseInt(cleanNum.substring(0, 4), 10);
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 NOUVELLE-AQUITAINE';
} 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 AUVERGNE-RHÔNE-ALPES';
} 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';
}
if (matchedBrandSlug && matchedBrandLabel) {
typeLabel = matchedBrandLabel;
if (!matchedLogo || matchedLogo.file === 'logo-ter.svg' || matchedLogo.file === 'logo-sncf.svg') {
matchedLogo = logoLookup.get(matchedBrandSlug) || matchedLogo;
}
}
}
}
const logoFile = (matchedLogo && matchedLogo.file) || 'logo-sncf.svg';
const logoSrc = `/type/${logoFile}`;
-12
View File
@@ -1,12 +0,0 @@
const horaire_date = "2026-06-14";
const baseDate = new Date(horaire_date);
console.log("baseDate before setHours:", baseDate.toString());
const sched = 330; // 05:30
baseDate.setHours(Math.floor(sched / 60), sched % 60, 0, 0);
console.log("baseDate after setHours:", baseDate.toString());
const currentTime = new Date("2026-06-14T04:49:15+02:00");
console.log("currentTime:", currentTime.toString());
const diffMinutes = (baseDate.getTime() - currentTime.getTime()) / 60000;
console.log("diffMinutes:", diffMinutes);
-17
View File
@@ -1,17 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const gareDisplay = "Ouges";
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
console.log("flexNeedle:", flexNeedle);
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type').ilike('nom', flexNeedle).limit(1);
console.log("gdata:", gdata);
let fetchedType = null;
if (!gerr && gdata && gdata.length) fetchedType = gdata[0] && gdata[0].type;
console.log("fetchedType:", fetchedType);
}
run();
-10
View File
@@ -1,10 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data } = await supabase.from('gares').select('id,nom,type').ilike('nom', '%Ouges%');
console.log("Matched gares:", data);
}
run();
-12
View File
@@ -1,12 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data: horaires, error: hErr } = await supabase.from('horaires').select('*').limit(2);
console.log("Horaires sample:", horaires);
const { data: perturbs, error: pErr } = await supabase.from('perturbations').select('*').limit(2);
console.log("Perturbations sample:", perturbs);
}
run();
-23
View File
@@ -1,23 +0,0 @@
const horaires = [{ heure_depart: '21:00', numero_train: '123' }];
const isArrival = false;
const currentTime = new Date();
currentTime.setHours(20, 55, 0, 0);
for (const train of horaires) {
const numTrain = train.num_train || train.numero_train || train.numero;
const timeStr = isArrival
? (train.heure_arrivee || train.arrival_time || train.time || train.heure)
: (train.heure_depart || train.departure_time || train.time || train.heure);
let trainTime;
if (typeof timeStr === 'string' && timeStr.length <= 5 && timeStr.includes(':')) {
const [hh, mm] = timeStr.split(':');
trainTime = new Date(currentTime);
trainTime.setHours(parseInt(hh, 10), parseInt(mm, 10), 0, 0);
} else {
trainTime = new Date(timeStr);
}
const diffMs = trainTime.getTime() - currentTime.getTime();
const diffMins = Math.floor(diffMs / 60000);
console.log(`numTrain=${numTrain}, timeStr=${timeStr}, diffMins=${diffMins}`);
}
-85
View File
@@ -1,85 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
const normalizeName = (s) => {
if (!s) return '';
try {
const t = String(s).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
return t.replace(/[-_]+/g, ' ').replace(/\s+/g, ' ');
} catch (e) {
return String(s).toLowerCase().trim();
}
};
const parseTimeHM = (v) => {
if (!v) return null;
const m = String(v).match(/(\d{1,2}):(\d{2})/);
if (m) return parseInt(m[1], 10)*60 + parseInt(m[2], 10);
return null;
};
async function run() {
const gareDisplay = "Ouges";
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
const { data: gdata } = await supabase.from('gares').select('id,type').ilike('nom', flexNeedle).limit(1);
const stationType = gdata && gdata.length ? gdata[0].type.toLowerCase() : null;
console.log("stationType:", stationType);
const thresholdForStationType = (typeGare) => {
const tG = String(typeGare || '').trim().toLowerCase();
if (tG === 'interurbaine' || tG.includes('interurb')) return 360;
if (tG === 'ville' || tG.includes('ville')) return 30;
return 30;
};
const threshold = thresholdForStationType(stationType);
console.log("threshold:", threshold);
const { data: allData } = await supabase.from('horaires').select('*').limit(200);
const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges"));
const h = horaires[0];
const stops = h.gares_desservies;
const targetStationName = gareDisplay;
const stopForStation = (stops || []).find(s => {
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
const ns = normalizeName(nm || '');
const target = normalizeName(targetStationName || '');
return (targetStationName && (ns === target || ns.includes(target) || target.includes(ns)));
}) || null;
h._displayPlatform = (stopForStation && stopForStation.quai_reel) || h.quai_reel || '';
console.log("_displayPlatform:", h._displayPlatform);
const platform = (h._displayPlatform || '').toString().trim();
let showPlatform = Boolean(platform);
console.log("Boolean(platform):", showPlatform);
if (showPlatform) {
let diffMinutes = null;
h._depHM = parseTimeHM(h.heure_depart);
const sched = h._depHM;
console.log("sched:", sched);
const currentTime = new Date();
if (sched !== null) {
const baseDate = new Date();
baseDate.setHours(Math.floor(sched / 60), sched % 60, 0, 0);
const nowHM = currentTime.getHours() * 60 + currentTime.getMinutes();
if (sched < 240 && nowHM > 1200) baseDate.setDate(baseDate.getDate() + 1);
else if (sched > 1200 && nowHM < 240) baseDate.setDate(baseDate.getDate() - 1);
diffMinutes = (baseDate.getTime() - currentTime.getTime()) / 60000;
}
console.log("diffMinutes:", diffMinutes);
if (diffMinutes !== null) {
showPlatform = (diffMinutes <= threshold);
} else {
showPlatform = false;
}
}
console.log("FINAL showPlatform:", showPlatform);
}
run();
-10
View File
@@ -1,10 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
const supabaseUrl = 'https://supabase-ferrovia.mr-patator.fr';
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc1OTUxMjAxLCJleHAiOjE5MzM2MzEyMDF9.ZFuzZSHVOMX-poU1fhWTjZ2sn7LFz9NYATTwJknlSr0';
const supabase = createClient(supabaseUrl, supabaseAnonKey);
async function test() {
const { data: horaires, error } = await supabase.from('horaires').select('*').eq('numero_train', '891807');
console.log(horaires[0]);
}
test();
-16
View File
@@ -1,16 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function test() {
const { data: horaires } = await supabase.from('horaires').select('*').limit(100);
const matched = horaires.filter(h => JSON.stringify(h).includes("Ouges"));
const h = matched[0];
console.log("Train:", h.numero_train);
console.log("Stops:", h.gares_desservies.map(s => ({
gare_nom: s.gare_nom,
quai_reel: s.quai_reel,
gare_id: s.gare_id
})));
}
test();
-10
View File
@@ -1,10 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data } = await supabase.from('perturbations').select('*');
console.log("Perturbations:", data);
}
run();
-10
View File
@@ -1,10 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
const supabaseUrl = 'https://supabase-ferrovia.mr-patator.fr';
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc1OTUxMjAxLCJleHAiOjE5MzM2MzEyMDF9.ZFuzZSHVOMX-poU1fhWTjZ2sn7LFz9NYATTwJknlSr0';
const supabase = createClient(supabaseUrl, supabaseAnonKey);
async function test() {
const { data: horaires, error } = await supabase.from('horaires_departs').select('*');
console.log('Error:', error);
}
test();
-24
View File
@@ -1,24 +0,0 @@
require('dotenv').config();
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
const channel = supabase
.channel('test_channel')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'annonces_diffusions' },
(payload) => {
console.log('Realtime Event Received!', payload);
}
)
.subscribe((status) => {
console.log('Subscription status:', status);
});
console.log('Waiting for events...');
// We will kill this process manually or let it timeout
setTimeout(() => {
console.log('Timeout. Exiting.');
process.exit(0);
}, 10000);
-10
View File
@@ -1,10 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data } = await supabase.from('horaires').select('numero_train, heure_depart, circule_dimanche').ilike('gares_desservies', '%Ouges%');
console.log("Trains Ouges on Sunday:", data);
}
run();
-10
View File
@@ -1,10 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data } = await supabase.from('horaires').select('numero_train, circule_dimanche').in('numero_train', ['891801', '891813']);
console.log("Trains Ouges on Sunday:", data);
}
run();
-11
View File
@@ -1,11 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, gares_desservies, circule_dimanche').limit(200);
const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges") && h.circule_dimanche === true);
console.log("Trains Ouges on Sunday:", horaires.length);
}
run();
-14
View File
@@ -1,14 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, heure_depart, gares_desservies, circule_dimanche').limit(200);
const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges") && h.circule_dimanche === true);
horaires.forEach(h => {
const stop = h.gares_desservies.find(s => s.gare_nom === 'Ouges');
console.log(`Train ${h.numero_train} departs Ouges at ${stop ? stop.heure_depart : h.heure_depart}`);
});
}
run();
-14
View File
@@ -1,14 +0,0 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config({ path: '/Users/mathisgrillot/IdeaProjects/afficheurs-ferrovia/.env' });
const supabase = createClient(process.env.REACT_APP_SUPABASE_URL, process.env.REACT_APP_SUPABASE_ANON_KEY);
async function run() {
const { data: allData } = await supabase.from('horaires').select('numero_train, gare_depart_nom, gare_arrivee_nom, heure_depart, gares_desservies, circule_dimanche').limit(200);
const horaires = allData.filter(h => JSON.stringify(h).includes("Ouges") && h.circule_dimanche === true);
horaires.forEach(h => {
const stop = h.gares_desservies.find(s => s.gare_nom === 'Ouges');
console.log(`Train ${h.numero_train} departs Ouges at ${stop ? stop.heure_depart : h.heure_depart} with quai_reel: ${stop ? stop.quai_reel : 'none'}`);
});
}
run();