diff --git a/public/type/data.json b/public/type/data.json
index 244b66d..585eebd 100644
--- a/public/type/data.json
+++ b/public/type/data.json
@@ -135,121 +135,121 @@
},
{
"file": "logo-rer-a.svg",
- "path": "/img/type/logo-rer-a.svg",
+ "path": "/logo-rer-a.svg",
"slug": "rer-a",
"name": "RER A"
},
{
"file": "logo-rer-b.svg",
- "path": "/img/type/logo-rer-b.svg",
+ "path": "/logo-rer-b.svg",
"slug": "rer-b",
"name": "RER B"
},
{
"file": "logo-rer-c.svg",
- "path": "/img/type/logo-rer-c.svg",
+ "path": "/logo-rer-c.svg",
"slug": "rer-c",
"name": "RER C"
},
{
"file": "logo-rer-d.svg",
- "path": "/img/type/logo-rer-d.svg",
+ "path": "/logo-rer-d.svg",
"slug": "rer-d",
"name": "RER D"
},
{
"file": "logo-rer-e.svg",
- "path": "/img/type/logo-rer-e.svg",
+ "path": "/logo-rer-e.svg",
"slug": "rer-e",
"name": "RER E"
},
{
"file": "logo-sbb.svg",
- "path": "/img/type/logo-sbb.svg",
+ "path": "/logo-sbb.svg",
"slug": "sbb",
"name": "SBB"
},
{
"file": "logo-sncb.svg",
- "path": "/img/type/logo-sncb.svg",
+ "path": "/logo-sncb.svg",
"slug": "sncb",
"name": "SNCB"
},
{
"file": "logo-sncf.svg",
- "path": "/img/type/logo-sncf.svg",
+ "path": "/logo-sncf.svg",
"slug": "sncf",
"name": "Sncf"
},
{
"file": "sncf-voyageurs-logo.png",
- "path": "/img/type/sncf-voyageurs-logo.png",
+ "path": "/sncf-voyageurs-logo.png",
"slug": "sncf-voyageurs-logo",
"name": "SNCF"
},
{
"file": "logo-sncf1985.svg",
- "path": "/img/type/logo-sncf1985.svg",
+ "path": "/logo-sncf1985.svg",
"slug": "sncf1985",
"name": "Sncf1985"
},
{
"file": "logo-sncf1992.svg",
- "path": "/img/type/logo-sncf1992.svg",
+ "path": "/logo-sncf1992.svg",
"slug": "sncf1992",
"name": "Sncf1992"
},
{
"file": "logo-t-h.svg",
- "path": "/img/type/logo-t-h.svg",
+ "path": "/logo-t-h.svg",
"slug": "t-h",
"name": "T H"
},
{
"file": "logo-t-j.svg",
- "path": "/img/type/logo-t-j.svg",
+ "path": "/logo-t-j.svg",
"slug": "t-j",
"name": "T J"
},
{
"file": "logo-t-k.svg",
- "path": "/img/type/logo-t-k.svg",
+ "path": "/logo-t-k.svg",
"slug": "t-k",
"name": "T K"
},
{
"file": "logo-t-l.svg",
- "path": "/img/type/logo-t-l.svg",
+ "path": "/logo-t-l.svg",
"slug": "t-l",
"name": "T L"
},
{
"file": "logo-t-n.svg",
- "path": "/img/type/logo-t-n.svg",
+ "path": "/logo-t-n.svg",
"slug": "t-n",
"name": "T N"
},
{
"file": "logo-t-p.svg",
- "path": "/img/type/logo-t-p.svg",
+ "path": "/logo-t-p.svg",
"slug": "t-p",
"name": "T P"
},
{
"file": "logo-t-r.svg",
- "path": "/img/type/logo-t-r.svg",
+ "path": "/logo-t-r.svg",
"slug": "t-r",
"name": "T R"
},
{
"file": "logo-t-u.svg",
- "path": "/img/type/logo-t-u.svg",
+ "path": "/logo-t-u.svg",
"slug": "t-u",
"name": "T U"
},
{
"file": "logo-teoz.svg",
- "path": "/img/type/logo-teoz.svg",
+ "path": "/logo-teoz.svg",
"slug": "teoz",
"name": "Teoz"
},
@@ -261,121 +261,121 @@
},
{
"file": "logo-ter-rhone-alpes.svg",
- "path": "/img/type/logo-ter-rhone-alpes.svg",
+ "path": "/logo-ter-rhone-alpes.svg",
"slug": "ter-rhone-alpes",
"name": "TER Rhone-Alpes"
},
{
"file": "logo-teralsace.svg",
- "path": "/img/type/logo-teralsace.svg",
+ "path": "/logo-teralsace.svg",
"slug": "teralsace",
"name": "Ter Alsace"
},
{
"file": "logo-teraquitaine.svg",
- "path": "/img/type/logo-teraquitaine.svg",
+ "path": "/logo-teraquitaine.svg",
"slug": "teraquitaine",
"name": "Ter Aquitaine"
},
{
"file": "logo-terbassenormandie.svg",
- "path": "/img/type/logo-terbassenormandie.svg",
+ "path": "/logo-terbassenormandie.svg",
"slug": "terbassenormandie",
"name": "Ter Basse Normandie"
},
{
"file": "logo-terbourgogne.svg",
- "path": "/img/type/logo-terbourgogne.svg",
+ "path": "/logo-terbourgogne.svg",
"slug": "terbourgogne",
"name": "Ter Bourgogne"
},
{
"file": "logo-terbretagne.svg",
- "path": "/img/type/logo-terbretagne.svg",
+ "path": "/logo-terbretagne.svg",
"slug": "terbretagne",
"name": "Ter Bretagne"
},
{
"file": "logo-tercentre.svg",
- "path": "/img/type/logo-tercentre.svg",
+ "path": "/logo-tercentre.svg",
"slug": "tercentre",
"name": "Ter Centre"
},
{
"file": "logo-terlanguedocroussillon.svg",
- "path": "/img/type/logo-terlanguedocroussillon.svg",
+ "path": "/logo-terlanguedocroussillon.svg",
"slug": "terlanguedocroussillon",
"name": "Ter Languedoc-Roussillon"
},
{
"file": "logo-termidipyrenees.svg",
- "path": "/img/type/logo-termidipyrenees.svg",
+ "path": "/logo-termidipyrenees.svg",
"slug": "termidipyrenees",
"name": "Ter Midi-Pyrenees"
},
{
"file": "logo-ternordpasdecalais.svg",
- "path": "/img/type/logo-ternordpasdecalais.svg",
+ "path": "/logo-ternordpasdecalais.svg",
"slug": "ternordpasdecalais",
"name": "Ter Nord-pas-de-Calais"
},
{
"file": "logo-terpoitoucharentes.svg",
- "path": "/img/type/logo-terpoitoucharentes.svg",
+ "path": "/logo-terpoitoucharentes.svg",
"slug": "terpoitoucharentes",
"name": "Ter Poitou Charentes"
},
{
"file": "logo-tgv.svg",
- "path": "/img/type/logo-tgv.svg",
+ "path": "/logo-tgv.svg",
"slug": "tgv",
"name": "TGV"
},
{
"file": "logo-inoui.svg",
- "path": "/img/type/logo-inoui.svg",
+ "path": "/logo-inoui.svg",
"slug": "inoui",
"name": "TGV InOui"
},
{
"file": "logo-lyria.svg",
- "path": "/img/type/logo-lyria.svg",
+ "path": "/logo-lyria.svg",
"slug": "lyria",
"name": "TGV Lyria"
},
{
"file": "logo-thalys.svg",
- "path": "/img/type/logo-thalys.svg",
+ "path": "/logo-thalys.svg",
"slug": "thalys",
"name": "Thalys"
},
{
"file": "logo-thello.svg",
- "path": "/img/type/logo-thello.svg",
+ "path": "/logo-thello.svg",
"slug": "thello",
"name": "Thello"
},
{
"file": "track-car.svg",
- "path": "/img/type/track-car.svg",
+ "path": "/track-car.svg",
"slug": "track-car",
"name": "Car TER"
},
{
"file": "logo-tramtrain.svg",
- "path": "/img/type/logo-tramtrain.svg",
+ "path": "/logo-tramtrain.svg",
"slug": "tramtrain",
"name": "Tramtrain"
},
{
"file": "logo-trenitalia.svg",
- "path": "/img/type/logo-trenitalia.svg",
+ "path": "/logo-trenitalia.svg",
"slug": "trenitalia",
"name": "Trenitalia"
},
{
"file": "logo-zou.svg",
- "path": "/img/type/logo-zou.svg",
+ "path": "/logo-zou.svg",
"slug": "zou",
"name": "Zou"
}
diff --git a/public/type/quais/data-color.json b/public/type/quais/data-color.json
index 1134e06..b4dc54d 100644
--- a/public/type/quais/data-color.json
+++ b/public/type/quais/data-color.json
@@ -3,379 +3,379 @@
"logos": [
{
"file": "logo-aleop-color.svg",
- "path": "/type/quais/logo-aleop-color.svg",
+ "path": "/logo-aleop-color.svg",
"slug": "aleop",
"name": "Aleop"
},
{
"file": "logo-auvergerhonealpes-color.svg",
- "path": "/type/quais/logo-auvergerhonealpes-color.svg",
+ "path": "/logo-auvergerhonealpes-color.svg",
"slug": "auvergerhonealpes",
"name": "Auvergerhonealpes"
},
{
"file": "logo-breizhgo-color.svg",
- "path": "/type/quais/logo-breizhgo-color.svg",
+ "path": "/logo-breizhgo-color.svg",
"slug": "breizhgo",
"name": "Breizhgo"
},
{
"file": "logo-cfl-color.svg",
- "path": "/type/quais/logo-cfl-color.svg",
+ "path": "/logo-cfl-color.svg",
"slug": "cfl",
"name": "CFL"
},
{
"file": "logo-db-color.svg",
- "path": "/type/quais/logo-db-color.svg",
+ "path": "/logo-db-color.svg",
"slug": "db",
"name": "DB"
},
{
"file": "logo-eurostar-color.svg",
- "path": "/type/quais/logo-eurostar-color.svg",
+ "path": "/logo-eurostar-color.svg",
"slug": "eurostar",
"name": "Eurostar"
},
{
"file": "logo-fluo-color.svg",
- "path": "/type/quais/logo-fluo-color.svg",
+ "path": "/logo-fluo-color.svg",
"slug": "fluo",
"name": "Fluo"
},
{
"file": "logo-frecciarossa-color.svg",
- "path": "/type/quais/logo-frecciarossa-color.svg",
+ "path": "/logo-frecciarossa-color.svg",
"slug": "frecciarossa",
"name": "Frecciarossa"
},
{
"file": "logo-hautsdefrance-color.svg",
- "path": "/type/quais/logo-hautsdefrance-color.svg",
+ "path": "/logo-hautsdefrance-color.svg",
"slug": "hautsdefrance",
"name": "Hautsdefrance"
},
{
"file": "logo-ice-color.svg",
- "path": "/type/quais/logo-ice-color.svg",
+ "path": "/logo-ice-color.svg",
"slug": "ice",
"name": "ICE"
},
{
"file": "logo-intercites-color.svg",
- "path": "/type/quais/logo-intercites-color.svg",
+ "path": "/logo-intercites-color.svg",
"slug": "intercites",
"name": "Intercités"
},
{
"file": "logo-ler-color.svg",
- "path": "/type/quais/logo-ler-color.svg",
+ "path": "/logo-ler-color.svg",
"slug": "ler",
"name": "Ler"
},
{
"file": "logo-lio-color.svg",
- "path": "/type/quais/logo-lio-color.svg",
+ "path": "/logo-lio-color.svg",
"slug": "lio",
"name": "Lio"
},
{
"file": "logo-lunea-color.svg",
- "path": "/type/quais/logo-lunea-color.svg",
+ "path": "/logo-lunea-color.svg",
"slug": "lunea",
"name": "Lunea"
},
{
"file": "logo-metrolor-color.svg",
- "path": "/type/quais/logo-metrolor-color.svg",
+ "path": "/logo-metrolor-color.svg",
"slug": "metrolor",
"name": "Metrolor"
},
{
"file": "logo-mobigo-color.svg",
- "path": "/type/quais/logo-mobigo-color.svg",
+ "path": "/logo-mobigo-color.svg",
"slug": "mobigo",
"name": "Mobigo"
},
{
"file": "logo-nomad-color.svg",
- "path": "/type/quais/logo-nomad-color.svg",
+ "path": "/logo-nomad-color.svg",
"slug": "nomad",
"name": "Nomad"
},
{
"file": "logo-occitanie-color.svg",
- "path": "/type/quais/logo-occitanie-color.svg",
+ "path": "/logo-occitanie-color.svg",
"slug": "occitanie",
"name": "Occitanie"
},
{
"file": "logo-ouigo-color.svg",
- "path": "/type/quais/logo-ouigo-color.svg",
+ "path": "/logo-ouigo-color.svg",
"slug": "ouigo",
"name": "OUIGO"
},
{
"file": "logo-ouigo-classique-color.svg",
- "path": "/type/quais/logo-ouigo-classique-color.svg",
+ "path": "/logo-ouigo-classique-color.svg",
"slug": "ouigo-classique",
"name": "OUIGO Trains Classiques"
},
{
"file": "logo-remi-color.svg",
- "path": "/type/quais/logo-remi-color.svg",
+ "path": "/logo-remi-color.svg",
"slug": "remi",
"name": "Remi"
},
{
"file": "logo-renfeave-color.svg",
- "path": "/type/quais/logo-renfeave-color.svg",
+ "path": "/logo-renfeave-color.svg",
"slug": "renfeave",
"name": "Renfe AVE"
},
{
"file": "logo-rer-a-color.svg",
- "path": "/type/quais/logo-rer-a-color.svg",
+ "path": "/logo-rer-a-color.svg",
"slug": "rer-a",
"name": "RER A"
},
{
"file": "logo-rer-b-color.svg",
- "path": "/type/quais/logo-rer-b-color.svg",
+ "path": "/logo-rer-b-color.svg",
"slug": "rer-b",
"name": "RER B"
},
{
"file": "logo-rer-c-color.svg",
- "path": "/type/quais/logo-rer-c-color.svg",
+ "path": "/logo-rer-c-color.svg",
"slug": "rer-c",
"name": "RER C"
},
{
"file": "logo-rer-d-color.svg",
- "path": "/type/quais/logo-rer-d-color.svg",
+ "path": "/logo-rer-d-color.svg",
"slug": "rer-d",
"name": "RER D"
},
{
"file": "logo-rer-e-color.svg",
- "path": "/type/quais/logo-rer-e-color.svg",
+ "path": "/logo-rer-e-color.svg",
"slug": "rer-e",
"name": "RER E"
},
{
"file": "logo-sbb-color.svg",
- "path": "/type/quais/logo-sbb-color.svg",
+ "path": "/logo-sbb-color.svg",
"slug": "sbb",
"name": "SBB"
},
{
"file": "logo-sncb-color.svg",
- "path": "/type/quais/logo-sncb-color.svg",
+ "path": "/logo-sncb-color.svg",
"slug": "sncb",
"name": "SNCB"
},
{
"file": "logo-sncf-color.svg",
- "path": "/type/quais/logo-sncf-color.svg",
+ "path": "/logo-sncf-color.svg",
"slug": "sncf",
"name": "Sncf"
},
{
"file": "sncf-voyageurs-logo-color.png",
- "path": "/type/quais/img/type/sncf-voyageurs-logo-color.png",
+ "path": "/img/type/sncf-voyageurs-logo-color.png",
"slug": "sncf-voyageurs-logo",
"name": "SNCF"
},
{
"file": "logo-sncf1985.svg",
- "path": "/type/quais/img/type/logo-sncf1985.svg",
+ "path": "/img/type/logo-sncf1985.svg",
"slug": "sncf1985",
"name": "Sncf1985"
},
{
"file": "logo-sncf1992.svg",
- "path": "/type/quais/img/type/logo-sncf1992.svg",
+ "path": "/img/type/logo-sncf1992.svg",
"slug": "sncf1992",
"name": "Sncf1992"
},
{
"file": "logo-t-h.svg",
- "path": "/type/quais/img/type/logo-t-h.svg",
+ "path": "/img/type/logo-t-h.svg",
"slug": "t-h",
"name": "T H"
},
{
"file": "logo-t-j.svg",
- "path": "/type/quais/img/type/logo-t-j.svg",
+ "path": "/img/type/logo-t-j.svg",
"slug": "t-j",
"name": "T J"
},
{
"file": "logo-t-k.svg",
- "path": "/type/quais/img/type/logo-t-k.svg",
+ "path": "/img/type/logo-t-k.svg",
"slug": "t-k",
"name": "T K"
},
{
"file": "logo-t-l.svg",
- "path": "/type/quais/img/type/logo-t-l.svg",
+ "path": "/img/type/logo-t-l.svg",
"slug": "t-l",
"name": "T L"
},
{
"file": "logo-t-n.svg",
- "path": "/type/quais/img/type/logo-t-n.svg",
+ "path": "/img/type/logo-t-n.svg",
"slug": "t-n",
"name": "T N"
},
{
"file": "logo-t-p.svg",
- "path": "/type/quais/img/type/logo-t-p.svg",
+ "path": "/img/type/logo-t-p.svg",
"slug": "t-p",
"name": "T P"
},
{
"file": "logo-t-r.svg",
- "path": "/type/quais/img/type/logo-t-r.svg",
+ "path": "/img/type/logo-t-r.svg",
"slug": "t-r",
"name": "T R"
},
{
"file": "logo-t-u.svg",
- "path": "/type/quais/img/type/logo-t-u.svg",
+ "path": "/img/type/logo-t-u.svg",
"slug": "t-u",
"name": "T U"
},
{
"file": "logo-teoz.svg",
- "path": "/type/quais/img/type/logo-teoz.svg",
+ "path": "/img/type/logo-teoz.svg",
"slug": "teoz",
"name": "Teoz"
},
{
"file": "sncf-voyageurs-logo.png",
- "path": "/type/quais/sncf-voyageurs-logo.png",
+ "path": "/sncf-voyageurs-logo.png",
"slug": "ter",
"name": "TER"
},
{
"file": "logo-ter-rhone-alpes.svg",
- "path": "/type/quais/logo-ter-rhone-alpes.svg",
+ "path": "/logo-ter-rhone-alpes.svg",
"slug": "ter-rhone-alpes",
"name": "TER Rhone Alpes"
},
{
"file": "logo-teralsace.svg",
- "path": "/type/quais/logo-teralsace.svg",
+ "path": "/logo-teralsace.svg",
"slug": "teralsace",
"name": "Teralsace"
},
{
"file": "logo-teraquitaine.svg",
- "path": "/type/quais/logo-teraquitaine.svg",
+ "path": "/logo-teraquitaine.svg",
"slug": "teraquitaine",
"name": "Teraquitaine"
},
{
"file": "logo-terbassenormandie.svg",
- "path": "/type/quais/logo-terbassenormandie.svg",
+ "path": "/logo-terbassenormandie.svg",
"slug": "terbassenormandie",
"name": "Terbassenormandie"
},
{
"file": "logo-terbourgogne.svg",
- "path": "/type/quais/logo-terbourgogne.svg",
+ "path": "/logo-terbourgogne.svg",
"slug": "terbourgogne",
"name": "Terbourgogne"
},
{
"file": "logo-terbretagne.svg",
- "path": "/type/quais/logo-terbretagne.svg",
+ "path": "/logo-terbretagne.svg",
"slug": "terbretagne",
"name": "Terbretagne"
},
{
"file": "logo-tercentre.svg",
- "path": "/type/quais/logo-tercentre.svg",
+ "path": "/logo-tercentre.svg",
"slug": "tercentre",
"name": "Tercentre"
},
{
"file": "logo-terlanguedocroussillon.svg",
- "path": "/type/quais/logo-terlanguedocroussillon.svg",
+ "path": "/logo-terlanguedocroussillon.svg",
"slug": "terlanguedocroussillon",
"name": "Terlanguedocroussillon"
},
{
"file": "logo-termidipyrenees.svg",
- "path": "/type/quais/logo-termidipyrenees.svg",
+ "path": "/logo-termidipyrenees.svg",
"slug": "termidipyrenees",
"name": "Termidipyrenees"
},
{
"file": "logo-ternordpasdecalais.svg",
- "path": "/type/quais/logo-ternordpasdecalais.svg",
+ "path": "/logo-ternordpasdecalais.svg",
"slug": "ternordpasdecalais",
"name": "Ternordpasdecalais"
},
{
"file": "logo-terpoitoucharentes.svg",
- "path": "/type/quais/logo-terpoitoucharentes.svg",
+ "path": "/logo-terpoitoucharentes.svg",
"slug": "terpoitoucharentes",
"name": "Terpoitoucharentes"
},
{
"file": "logo-tgv.svg",
- "path": "/type/quais/logo-tgv.svg",
+ "path": "/logo-tgv.svg",
"slug": "tgv",
"name": "TGV"
},
{
"file": "logo-inoui.svg",
- "path": "/type/quais/logo-inoui.svg",
+ "path": "/logo-inoui.svg",
"slug": "inoui",
"name": "TGV InOui"
},
{
"file": "logo-lyria.svg",
- "path": "/type/quais/logo-lyria.svg",
+ "path": "/logo-lyria.svg",
"slug": "lyria",
"name": "TGV Lyria"
},
{
"file": "logo-thalys.svg",
- "path": "/type/quais/logo-thalys.svg",
+ "path": "/logo-thalys.svg",
"slug": "thalys",
"name": "Thalys"
},
{
"file": "logo-thello.svg",
- "path": "/type/quais/logo-thello.svg",
+ "path": "/logo-thello.svg",
"slug": "thello",
"name": "Thello"
},
{
"file": "track-car.svg",
- "path": "/type/quais/track-car.svg",
+ "path": "/track-car.svg",
"slug": "track-car",
"name": "Track Car"
},
{
"file": "logo-tramtrain.svg",
- "path": "/type/quais/logo-tramtrain.svg",
+ "path": "/logo-tramtrain.svg",
"slug": "tramtrain",
"name": "Tramtrain"
},
{
"file": "logo-trenitalia.svg",
- "path": "/type/quais/logo-trenitalia.svg",
+ "path": "/logo-trenitalia.svg",
"slug": "trenitalia",
"name": "Trenitalia"
},
{
"file": "logo-zou.svg",
- "path": "/type/quais/logo-zou.svg",
+ "path": "/logo-zou.svg",
"slug": "zou",
"name": "Zou"
}
diff --git a/src/components/afficheurs/TgvCompositionQuai.css b/src/components/afficheurs/TgvCompositionQuai.css
new file mode 100644
index 0000000..7813562
--- /dev/null
+++ b/src/components/afficheurs/TgvCompositionQuai.css
@@ -0,0 +1,298 @@
+/* =========================================================
+ BANDEAU COMPOSITION TGV (SNCF QUAI - Conforme Image 1 & 2)
+ ========================================================= */
+
+.aff-tgv-comp__container {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 1920px;
+ height: 240px;
+ background-color: #4a0e4e; /* Couleur Prune / Aubergine SNCF authentique */
+ color: #ffffff;
+ font-family: 'Achemine', 'Avenir', 'Helvetica Neue', Arial, sans-serif;
+ box-sizing: border-box;
+ padding: 10px 35px 8px 35px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ z-index: 25;
+ overflow: hidden;
+}
+
+/* =========================================================
+ TOP ROW : BOX VOIE + RAMES TGV
+ ========================================================= */
+.aff-tgv-comp__top-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ height: 135px;
+}
+
+/* Box Voie à gauche */
+.aff-tgv-comp__track-box {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 120px;
+ min-width: 120px;
+}
+
+.aff-tgv-comp__track-pill {
+ background-color: #ffffff;
+ color: #4a0e4e;
+ font-size: 19px;
+ font-weight: 800;
+ text-transform: uppercase;
+ padding: 2px 14px;
+ border-radius: 12px;
+ line-height: 1.1;
+ letter-spacing: 0.5px;
+}
+
+.aff-tgv-comp__track-letter {
+ color: #ffffff;
+ font-size: 64px;
+ font-weight: 800;
+ line-height: 1;
+ margin-top: 4px;
+ margin-bottom: 4px;
+ text-align: center;
+}
+
+.aff-tgv-comp__track-underline {
+ width: 65px;
+ height: 3.5px;
+ background-color: #ffffff;
+ border-radius: 2px;
+}
+
+/* Conteneur des rames TGV */
+.aff-tgv-comp__rames-container {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 40px;
+ flex: 1;
+}
+
+/* Bloc d'une Rame */
+.aff-tgv-comp__rame-block {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+/* Titre Destination au-dessus de la rame (jaune doré) */
+.aff-tgv-comp__dest-title {
+ color: #f5d130;
+ font-size: 24px;
+ font-weight: 700;
+ line-height: 1.1;
+ margin-bottom: 6px;
+ text-align: center;
+ white-space: nowrap;
+}
+
+/* Ligne des numéros de voitures */
+.aff-tgv-comp__coaches-row {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ margin-bottom: 4px;
+}
+
+.aff-tgv-comp__coach-num-motrice {
+ width: 44px;
+ min-width: 44px;
+}
+
+.aff-tgv-comp__coach-num-box {
+ flex: 1;
+ min-width: 44px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #ffffff;
+ font-size: 19px;
+ font-weight: 700;
+ letter-spacing: 0.5px;
+}
+
+/* Graphic de la rame TGV */
+.aff-tgv-comp__train-graphic {
+ display: flex;
+ align-items: center;
+ height: 32px;
+ width: 100%;
+}
+
+/* Motrice avant */
+.aff-tgv-comp__loco-front {
+ width: 44px;
+ height: 30px;
+ background: linear-gradient(180deg, #813988 0%, #5d1d64 100%);
+ border-top-left-radius: 16px;
+ border-bottom-left-radius: 12px;
+ margin-right: 2px;
+}
+
+/* Corps des voitures */
+.aff-tgv-comp__coaches-body {
+ display: flex;
+ height: 30px;
+ flex: 1;
+}
+
+.aff-tgv-comp__coach-segment {
+ flex: 1;
+ min-width: 44px;
+ height: 30px;
+ background-color: #b776bd;
+ border-right: 2px solid #ffffff;
+ position: relative;
+}
+
+.aff-tgv-comp__coach-segment:first-child {
+ border-top-left-radius: 3px;
+ border-bottom-left-radius: 3px;
+}
+
+.aff-tgv-comp__coach-segment:last-child {
+ border-right: none;
+ border-top-right-radius: 3px;
+ border-bottom-right-radius: 3px;
+}
+
+.aff-tgv-comp__coach-segment--bar {
+ background-color: #c98cd0;
+}
+
+/* Motrice arrière */
+.aff-tgv-comp__loco-rear {
+ width: 44px;
+ height: 30px;
+ background: linear-gradient(180deg, #813988 0%, #5d1d64 100%);
+ border-top-right-radius: 16px;
+ border-bottom-right-radius: 12px;
+ margin-left: 2px;
+}
+
+/* =========================================================
+ LIGNE POINTILLÉE AVEC REPÈRES DE QUAI (Conforme Image 2)
+ ========================================================= */
+.aff-tgv-comp__reperes-line-wrapper {
+ position: relative;
+ width: 100%;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ margin: 2px 0 4px 0;
+}
+
+.aff-tgv-comp__dotted-line {
+ position: absolute;
+ top: 50%;
+ left: 0;
+ right: 0;
+ border-bottom: 2.5px dotted #f5d130;
+ transform: translateY(-50%);
+ z-index: 1;
+}
+
+.aff-tgv-comp__reperes-row {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 36px;
+ width: 100%;
+ padding-right: 40px;
+ box-sizing: border-box;
+ z-index: 2;
+}
+
+.aff-tgv-comp__repere-box {
+ background-color: #00337f;
+ border: 1.5px solid #ffffff;
+ border-radius: 4px;
+ color: #ffffff;
+ font-weight: 800;
+ font-size: 21px;
+ line-height: 1;
+ width: 32px;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* =========================================================
+ BOTTOM ROW : MARQUEE + DIGITAL CLOCK
+ ========================================================= */
+.aff-tgv-comp__bottom-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 52px;
+ width: 100%;
+}
+
+.aff-tgv-comp__marquee-wrapper {
+ flex: 1;
+ overflow: hidden;
+ white-space: nowrap;
+ margin-right: 30px;
+}
+
+.aff-tgv-comp__marquee-text {
+ display: inline-block;
+ color: #ffffff;
+ font-style: italic;
+ font-weight: 700;
+ font-size: 29px;
+ letter-spacing: 1px;
+ text-transform: uppercase;
+ animation: affTgvMarquee 25s linear infinite;
+}
+
+@keyframes affTgvMarquee {
+ 0% {
+ transform: translateX(100%);
+ }
+ 100% {
+ transform: translateX(-100%);
+ }
+}
+
+/* Horloge TGV */
+.aff-tgv-comp__clock-wrapper {
+ display: flex;
+ align-items: center;
+}
+
+.aff-tgv-comp__clock {
+ background-color: #00337f;
+ border: 2px solid #ffffff;
+ border-radius: 4px;
+ padding: 3px 14px;
+ display: flex;
+ align-items: baseline;
+ gap: 4px;
+ font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif;
+ font-size: 34px;
+ font-weight: 700;
+ line-height: 1;
+}
+
+.aff-tgv-comp__clock-hm {
+ color: #ffffff;
+}
+
+.aff-tgv-comp__clock-s {
+ color: #f5a623;
+ margin-left: 2px;
+}
diff --git a/src/components/afficheurs/TgvCompositionQuai.js b/src/components/afficheurs/TgvCompositionQuai.js
new file mode 100644
index 0000000..08f58c5
--- /dev/null
+++ b/src/components/afficheurs/TgvCompositionQuai.js
@@ -0,0 +1,158 @@
+import React, { useEffect, useState } from 'react';
+import './TgvCompositionQuai.css';
+
+/**
+ * Icône Bar / Coupe de verre
+ */
+function BarGlassIcon({ size = 20, color = '#ffffff' }) {
+ return (
+
+ );
+}
+
+/**
+ * Horloge digitale avec secondes (conformité Image 1 & 2)
+ */
+function TgvDigitalClock() {
+ const [now, setNow] = useState(new Date());
+ useEffect(() => {
+ const id = setInterval(() => setNow(new Date()), 1000);
+ return () => clearInterval(id);
+ }, []);
+ const hh = String(now.getHours()).padStart(2, '0');
+ const mm = String(now.getMinutes()).padStart(2, '0');
+ const ss = String(now.getSeconds()).padStart(2, '0');
+ return (
+
+ {hh}:{mm}
+ {ss}
+
+ );
+}
+
+/**
+ * Dessin d'une rame TGV de 8 voitures avec motrice avant, arrière et voiture Bar en position 4
+ */
+function TgvRameShape({ destination, coaches = ['01', '02', '03', 'bar', '05', '06', '07', '08'] }) {
+ return (
+
+ {destination && (
+
+ {destination}
+
+ )}
+
+ {/* Numéros des voitures */}
+
+ {/* Espace motrice avant */}
+
+
+ {coaches.map((c, idx) => (
+
+ {c === 'bar' ? (
+
+ ) : (
+ {c}
+ )}
+
+ ))}
+
+ {/* Espace motrice arrière */}
+
+
+
+ {/* Silhouette du train TGV */}
+
+ {/* Motrice avant arrondie */}
+
+
+ {/* Voitures 1 à 8 */}
+
+ {coaches.map((c, idx) => (
+
+ ))}
+
+
+ {/* Motrice arrière arrondie */}
+
+
+
+ );
+}
+
+export default function TgvCompositionQuai({
+ platform = '',
+ destination = '',
+ destination2 = '',
+ marqueeMessage = '',
+ composition = null,
+ reperes = ['R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
+}) {
+ const cleanTrack = String(platform || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
+
+ // Détermination des tranches (Rame 1 et Rame 2)
+ const isMultiDest = Boolean(destination2 && destination2.trim() !== '' && destination2.toLowerCase() !== destination.toLowerCase());
+
+ const rame1Coaches = (composition && composition.rame1) || ['01', '02', '03', 'bar', '05', '06', '07', '08'];
+ const rame2Coaches = (composition && composition.rame2) || ['11', '12', '13', 'bar', '15', '16', '17', '18'];
+
+ const defaultMarquee = marqueeMessage || 'MERCI DE VOTRE COMPRÉHENSION..........................................................................................';
+
+ return (
+
+ {/* LIGNE DU HAUT : BOX VOIE + VISUALISATION DU TRAIN */}
+
+ {/* Indicateur Voie à gauche */}
+
+
Voie
+
{cleanTrack ? cleanTrack.toUpperCase() : '-'}
+
+
+
+ {/* Visualisation Composition des Rames */}
+
+
+
+
+
+
+ {/* LIGNE POINTILLÉE AVEC REPÈRES DE QUAI (Conforme Image 2) */}
+
+
+ {reperes && reperes.length > 0 && (
+
+ {reperes.map((rep, idx) => (
+
+ {rep}
+
+ ))}
+
+ )}
+
+
+ {/* LIGNE DU BAS : DÉFILEMENT MESSAGE + HORLOGE */}
+
+
+ );
+}
diff --git a/src/pages/afficheurs/afl/AFLArrivees.js b/src/pages/afficheurs/afl/AFLArrivees.js
index 3765d51..51c3f3b 100644
--- a/src/pages/afficheurs/afl/AFLArrivees.js
+++ b/src/pages/afficheurs/afl/AFLArrivees.js
@@ -6,7 +6,8 @@ import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService';
-import { getSncfTimetable } from '../../../services/sncfApi';
+import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi';
+import { matchStationNameStrict } from '../../../services/gtfsStations';
import AFLSecurityPage from './components/AFLSecurityPage';
import AFLCorrespondancesPage from './components/AFLCorrespondancesPage';
import AFLInformationsPage from './components/AFLInformationsPage';
@@ -219,12 +220,8 @@ export default function AFLArrivees(){
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
if (!gerr && gdata && gdata.length) {
- const exactMatch = gdata.find(g => {
- const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
- const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
- return n1 === n2;
- });
- const chosen = exactMatch || gdata[0];
+ const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
+ const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
fetchedType = chosen ? chosen.type : null;
fetchedId = chosen ? chosen.id : null;
}
@@ -662,17 +659,18 @@ export default function AFLArrivees(){
const stopForStation = (stops || []).find(s => {
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
- const ns = normalizeName(nm || '');
- const target = normalizeName(targetStationName || '');
- return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
+ return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
}) || null;
horaire._status = displayStatus;
horaire._displayTime = displayTime;
- const platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || '';
- horaire._displayPlatform = String(platformVal).trim();
+ let platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform || stopForStation.voie)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || horaire.voie || '';
+ if (!platformVal) {
+ platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
+ }
+ horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
return horaire;
});
- }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
+ }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
// Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu
useEffect(() => {
@@ -699,11 +697,13 @@ export default function AFLArrivees(){
} else {
const formatted = (sncfData || []).map(t => {
const timeStr = t.heure_arrivee || t.heure_depart || t.time || '';
+ const arrHM = (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr);
+ const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
return {
...t,
_displayTime: formatTimeNoSeconds(timeStr),
- _displayPlatform: t.quai || t.voie || '',
- _arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr),
+ _displayPlatform: plat,
+ _arrHM: arrHM,
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
@@ -784,25 +784,24 @@ export default function AFLArrivees(){
// comme une gare d'arrivée (soit le terminus, soit un arrêt intermédiaire)
let displayHoraires = parsedHoraires.slice();
if (gareDisplay) {
- const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
displayHoraires = displayHoraires.filter(h => {
// Exclure si la gare ciblée est la gare de départ (on veut les arrivées, pas les départs)
- const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString());
- if (departName === normalized) return false;
+ const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
+ if (matchStationNameStrict(departName, gareDisplay)) return false;
- // Inclure si gare d'arrivée finale correspond
- const arriveeName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
- if (arriveeName === normalized) return true;
+ // Inclure si gare d'arrivée finale correspond
+ const arriveeName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
+ if (matchStationNameStrict(arriveeName, gareDisplay)) return true;
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est l'origine (isFirst ou index==0)
const stopsList = getStops(h);
if (Array.isArray(stopsList) && stopsList.length) {
for (const s of stopsList) {
- const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
+ const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString();
if (!stopName) continue;
const isOrigin = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && Number(s.index) === 0));
if (isOrigin) continue; // Si c'est l'origine, ça ne peut pas être une arrivée
- if (stopName === normalized) return true;
+ if (matchStationNameStrict(stopName, gareDisplay)) return true;
}
}
return false;
@@ -868,7 +867,7 @@ export default function AFLArrivees(){
const stopsListForH = getStops(h);
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null;
- const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null;
+ const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null;
const stop = byId || byName;
if (stop && (stop.heure_arrivee || stop.time || stop.heure)) raw = stop.heure_arrivee || stop.time || stop.heure;
}
@@ -1300,7 +1299,17 @@ export default function AFLArrivees(){
).toString().trim();
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
- const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
+ const isCarTER = (
+ op === 'CAR TER' ||
+ op === 'AUTOCAR' ||
+ op.includes('AUTOCAR') ||
+ op.includes('CAR') ||
+ op.includes('BUS') ||
+ rawPlatform === 'AUTOCAR' ||
+ rawPlatform === 'CAR' ||
+ rawPlatform === 'BUS' ||
+ isSub
+ );
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure || '');
const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue';
diff --git a/src/pages/afficheurs/afl/AFLArriveesQuais.js b/src/pages/afficheurs/afl/AFLArriveesQuais.js
index f54bd70..05c7ffa 100644
--- a/src/pages/afficheurs/afl/AFLArriveesQuais.js
+++ b/src/pages/afficheurs/afl/AFLArriveesQuais.js
@@ -6,6 +6,7 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import Logo from '../../../components/Logo';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
+import { matchStationNameStrict } from '../../../services/gtfsStations';
import GareVideoprotection from '../../../components/afficheurs/GareVideoprotection';
import AttentifsEnsemble from '../../../components/afficheurs/AttentifsEnsemble';
import AFLInformationsPage from './components/AFLInformationsPage';
@@ -373,22 +374,36 @@ const AFLArriveesQuais = () => {
quai: quaiParam
});
if (sncfData && sncfData.length > 0) {
- const train = sncfData[0];
- const enriched = {
- ...train,
- _rawTime: train.heure_arrivee || train.heure_depart,
- _displayTime: formatTimeNoSeconds(train.heure_arrivee || train.heure_depart),
- _status: {
- label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
- detail: '',
- delayMinutes: train.retard_minutes || null
- },
- _nextStationStatus: {
- label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure")
- },
- _isToday: true
- };
- setNextArrival(enriched);
+ let matching = sncfData;
+ if (quaiParam) {
+ const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
+ matching = sncfData.filter(t => cleanP(t._displayPlatform || t.quai || t.voie) === cleanP(quaiParam));
+ }
+ const train = matching[0];
+ if (train) {
+ const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || '';
+ const enriched = {
+ ...train,
+ quai: trainPlatform,
+ quai_reel: trainPlatform,
+ voie: trainPlatform,
+ _displayPlatform: trainPlatform,
+ _rawTime: train.heure_arrivee || train.heure_depart,
+ _displayTime: formatTimeNoSeconds(train.heure_arrivee || train.heure_depart),
+ _status: {
+ label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
+ detail: '',
+ delayMinutes: train.retard_minutes || null
+ },
+ _nextStationStatus: {
+ label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure")
+ },
+ _isToday: true
+ };
+ setNextArrival(enriched);
+ } else {
+ setNextArrival(null);
+ }
} else {
setNextArrival(null);
}
@@ -591,25 +606,24 @@ const AFLArriveesQuais = () => {
// Filter by station: keep arrivals to this station or stops at this station (excluding origin)
let stationFiltered = enriched;
if (gareDisplay) {
- const normalized = normalizeName(gareDisplay);
stationFiltered = enriched.filter(h => {
// Exclude if target station is the departure origin
- const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString());
- if (departName === normalized) return false;
+ const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
+ if (matchStationNameStrict(departName, gareDisplay)) return false;
// Include if target station is the terminus
- const arriveeName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
- if (arriveeName === normalized) return true;
+ const arriveeName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
+ if (matchStationNameStrict(arriveeName, gareDisplay)) return true;
// Check intermediate stops
const stopsList = getStops(h);
if (Array.isArray(stopsList) && stopsList.length) {
for (const s of stopsList) {
- const stopName = normalizeName((s.gare_nom || s.nom || s.name || '').toString());
+ const stopName = (s.gare_nom || s.nom || s.name || '').toString();
if (!stopName) continue;
const isOrigin = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && Number(s.index) === 0));
if (isOrigin) continue;
- if (stopName === normalized) return true;
+ if (matchStationNameStrict(stopName, gareDisplay)) return true;
}
}
return false;
@@ -676,7 +690,7 @@ const AFLArriveesQuais = () => {
if (gareDisplay && Array.isArray(stopsList)) {
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
}
- const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
+ const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
if (!quaiParam) return true;
const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
return cleanP(platform) === cleanP(quaiParam);
@@ -862,10 +876,9 @@ const AFLArriveesQuais = () => {
let journeyStops = [];
if (Array.isArray(stopsList) && stopsList.length > 0) {
- const normCurrent = normalizeName(currentStationName);
const endIdx = stopsList.findIndex(s => {
- const sName = normalizeName((s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString());
- return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName));
+ const sName = (s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString();
+ return sName && matchStationNameStrict(sName, currentStationName);
});
const rawStops = endIdx !== -1 ? stopsList.slice(0, endIdx + 1) : stopsList.slice();
diff --git a/src/pages/afficheurs/afl/AFLDeparts.js b/src/pages/afficheurs/afl/AFLDeparts.js
index 7911fe4..883cebe 100644
--- a/src/pages/afficheurs/afl/AFLDeparts.js
+++ b/src/pages/afficheurs/afl/AFLDeparts.js
@@ -6,7 +6,8 @@ import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService';
-import { getSncfTimetable } from '../../../services/sncfApi';
+import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi';
+import { matchStationNameStrict } from '../../../services/gtfsStations';
import AFLSecurityPage from './components/AFLSecurityPage';
import AFLCorrespondancesPage from './components/AFLCorrespondancesPage';
import AFLInformationsPage from './components/AFLInformationsPage';
@@ -227,12 +228,8 @@ export default function AFLDeparts(){
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
if (!gerr && gdata && gdata.length) {
- const exactMatch = gdata.find(g => {
- const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
- const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
- return n1 === n2;
- });
- const chosen = exactMatch || gdata[0];
+ const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
+ const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
fetchedType = chosen ? chosen.type : null;
fetchedId = chosen ? chosen.id : null;
}
@@ -670,17 +667,18 @@ export default function AFLDeparts(){
const stopForStation = (stops || []).find(s => {
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
- const ns = normalizeName(nm || '');
- const target = normalizeName(targetStationName || '');
- return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
+ return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
}) || null;
horaire._status = displayStatus;
horaire._displayTime = displayTime;
- const platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || '';
- horaire._displayPlatform = String(platformVal).trim();
+ let platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform || stopForStation.voie)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || horaire.voie || '';
+ if (!platformVal) {
+ platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
+ }
+ horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
return horaire;
});
- }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
+ }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
useEffect(() => {
const id = setInterval(() => setShowStatus(s => !s), 3000);
@@ -710,18 +708,22 @@ export default function AFLDeparts(){
if (sncfErr) {
setError(sncfErr);
} else {
- const formatted = (sncfData || []).map(t => ({
- ...t,
- _displayTime: formatTimeNoSeconds(t.heure_depart),
- _displayPlatform: t.quai || t.voie || '',
- _depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart),
- _status: t._status || {
- label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
- detail: '',
- delayMinutes: t.retard_minutes || null,
- causeDetails: t.cause_retard || null
- }
- }));
+ const formatted = (sncfData || []).map(t => {
+ const depHM = (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart);
+ const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
+ return {
+ ...t,
+ _displayTime: formatTimeNoSeconds(t.heure_depart),
+ _displayPlatform: plat,
+ _depHM: depHM,
+ _status: t._status || {
+ label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
+ detail: '',
+ delayMinutes: t.retard_minutes || null,
+ causeDetails: t.cause_retard || null
+ }
+ };
+ });
setHoraires(formatted);
setAllHoraires(formatted);
}
@@ -795,15 +797,14 @@ export default function AFLDeparts(){
// Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
let displayHoraires = parsedHoraires.slice();
if (gareDisplay) {
- const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
displayHoraires = displayHoraires.filter(h => {
// Exclure si la gare ciblée est la gare d'arrivée finale
- const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
- if (destName === normalized) return false;
+ const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
+ if (matchStationNameStrict(destName, gareDisplay)) return false;
// Inclure si gare de départ correspond
- const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString());
- if (departName === normalized) return true;
+ const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString();
+ if (matchStationNameStrict(departName, gareDisplay)) return true;
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max)
const stopsList = getStops(h);
@@ -816,11 +817,11 @@ export default function AFLDeparts(){
}
}
for (const s of stopsList) {
- const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
+ const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString();
if (!stopName) continue;
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
if (isTerminus) continue;
- if (stopName === normalized) return true;
+ if (matchStationNameStrict(stopName, gareDisplay)) return true;
}
}
return false;
@@ -893,7 +894,7 @@ export default function AFLDeparts(){
const stopsListForH = getStops(h);
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null;
- const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null;
+ const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null;
const stop = byId || byName;
if (stop && (stop.heure_depart || stop.time || stop.heure)) raw = stop.heure_depart || stop.time || stop.heure;
}
@@ -1325,7 +1326,17 @@ export default function AFLDeparts(){
).toString().trim();
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
- const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
+ const isCarTER = (
+ op === 'CAR TER' ||
+ op === 'AUTOCAR' ||
+ op.includes('AUTOCAR') ||
+ op.includes('CAR') ||
+ op.includes('BUS') ||
+ rawPlatform === 'AUTOCAR' ||
+ rawPlatform === 'CAR' ||
+ rawPlatform === 'BUS' ||
+ isSub
+ );
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
const destination = horaire.gare_arrivee_nom || '';
diff --git a/src/pages/afficheurs/afl/AFLDepartsQuais.js b/src/pages/afficheurs/afl/AFLDepartsQuais.js
index 0ceb318..ec17229 100644
--- a/src/pages/afficheurs/afl/AFLDepartsQuais.js
+++ b/src/pages/afficheurs/afl/AFLDepartsQuais.js
@@ -6,6 +6,7 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import Logo from '../../../components/Logo';
import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi';
+import { matchStationNameStrict } from '../../../services/gtfsStations';
import GareVideoprotection from '../../../components/afficheurs/GareVideoprotection';
import AttentifsEnsemble from '../../../components/afficheurs/AttentifsEnsemble';
import DepartEnCoursAFL from '../../../components/afficheurs/DepartEnCoursAFL';
@@ -445,22 +446,36 @@ const AFLDepartsQuais = () => {
quai: quaiParam
});
if (sncfData && sncfData.length > 0) {
- const train = sncfData[0];
- const enriched = {
- ...train,
- _rawTime: train.heure_depart,
- _displayTime: formatTimeNoSeconds(train.heure_depart),
- _status: {
- label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
- detail: '',
- delayMinutes: train.retard_minutes || null
- },
- _nextStationStatus: {
- label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure")
- },
- _isToday: true
- };
- setNextDeparture(enriched);
+ let matching = sncfData;
+ if (quaiParam) {
+ const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
+ matching = sncfData.filter(t => cleanP(t._displayPlatform || t.quai || t.voie) === cleanP(quaiParam));
+ }
+ const train = matching[0];
+ if (train) {
+ const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || '';
+ const enriched = {
+ ...train,
+ quai: trainPlatform,
+ quai_reel: trainPlatform,
+ voie: trainPlatform,
+ _displayPlatform: trainPlatform,
+ _rawTime: train.heure_depart,
+ _displayTime: formatTimeNoSeconds(train.heure_depart),
+ _status: {
+ label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
+ detail: '',
+ delayMinutes: train.retard_minutes || null
+ },
+ _nextStationStatus: {
+ label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure")
+ },
+ _isToday: true
+ };
+ setNextDeparture(enriched);
+ } else {
+ setNextDeparture(null);
+ }
} else {
setNextDeparture(null);
}
@@ -663,13 +678,12 @@ const AFLDepartsQuais = () => {
// Filter by station: keep departures from this station or stops at this station (excluding terminus)
let stationFiltered = enriched;
if (gareDisplay) {
- const normalized = normalizeName(gareDisplay);
stationFiltered = enriched.filter(h => {
- const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
- if (destName === normalized) return false;
+ const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
+ if (matchStationNameStrict(destName, gareDisplay)) return false;
- const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString());
- if (departName === normalized) return true;
+ const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
+ if (matchStationNameStrict(departName, gareDisplay)) return true;
const stopsList = getStops(h);
if (Array.isArray(stopsList) && stopsList.length) {
@@ -681,11 +695,11 @@ const AFLDepartsQuais = () => {
}
}
for (const s of stopsList) {
- const stopName = normalizeName((s.gare_nom || s.nom || s.name || '').toString());
+ const stopName = (s.gare_nom || s.nom || s.name || '').toString();
if (!stopName) continue;
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
if (isTerminus) continue;
- if (stopName === normalized) return true;
+ if (matchStationNameStrict(stopName, gareDisplay)) return true;
}
}
return false;
@@ -751,7 +765,7 @@ const AFLDepartsQuais = () => {
if (gareDisplay && Array.isArray(stopsList)) {
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
}
- const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
+ const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
if (!quaiParam) return true;
const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
return cleanP(platform) === cleanP(quaiParam);
@@ -964,10 +978,9 @@ const AFLDepartsQuais = () => {
let downstreamStops = [];
if (Array.isArray(stopsList) && stopsList.length > 0) {
- const normCurrent = normalizeName(currentStationName);
const startIdx = stopsList.findIndex(s => {
- const sName = normalizeName((s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString());
- return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName));
+ const sName = (s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString();
+ return sName && matchStationNameStrict(sName, currentStationName);
});
const rawDownstream = startIdx !== -1 ? stopsList.slice(startIdx + 1) : stopsList;
@@ -976,7 +989,7 @@ const AFLDepartsQuais = () => {
rawDownstream.forEach(s => {
const name = (s.gare_nom || s.nom || s.name || s.station_name || s.stop_name || (typeof s === 'string' ? s : '')).toString().trim();
const norm = normalizeName(name);
- if (name && norm !== normCurrent && !seen.has(norm)) {
+ if (name && !matchStationNameStrict(name, currentStationName) && !seen.has(norm)) {
seen.add(norm);
downstreamStops.push(name);
}
diff --git a/src/pages/afficheurs/classiques/ClassiquesArrivees.css b/src/pages/afficheurs/classiques/ClassiquesArrivees.css
index f31b982..7317283 100644
--- a/src/pages/afficheurs/classiques/ClassiquesArrivees.css
+++ b/src/pages/afficheurs/classiques/ClassiquesArrivees.css
@@ -248,11 +248,25 @@
.aff-classiques-arrivees__status-delayed {
color: var(--yellow);
- font-size: 30px;
- font-weight: 700;
- line-height: 1.1;
- white-space: nowrap;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
font-family: "Achemine", sans-serif;
+ line-height: 1.05;
+}
+
+.aff-classiques-arrivees__status-delayed-label {
+ font-size: 24px;
+ font-weight: 700;
+ color: var(--yellow);
+ text-transform: lowercase;
+}
+
+.aff-classiques-arrivees__status-delayed-time {
+ font-size: 26px;
+ font-weight: 700;
+ color: var(--yellow);
}
.aff-classiques-arrivees__status-suppressed {
diff --git a/src/pages/afficheurs/classiques/ClassiquesArrivees.js b/src/pages/afficheurs/classiques/ClassiquesArrivees.js
index 054114d..464e116 100644
--- a/src/pages/afficheurs/classiques/ClassiquesArrivees.js
+++ b/src/pages/afficheurs/classiques/ClassiquesArrivees.js
@@ -6,9 +6,23 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { fetchAllRows } from '../../../lib/fetchAllRows';
import { getSourceFromPath } from '../../../services/railwayService';
-import { getSncfTimetable } from '../../../services/sncfApi';
+import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi';
+import { matchStationNameStrict } from '../../../services/gtfsStations';
import { getTrainLogoAndMeta } from '../../../utils/trainLogoHelper';
+// Pictogramme Autocar SNCF / MOBIGO
+const AutocarIcon = ({ size = '50px', color = '#ffffff', style = {} }) => (
+
+);
+
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
@@ -212,12 +226,8 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
if (!gerr && gdata && gdata.length) {
- const exactMatch = gdata.find(g => {
- const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
- const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
- return n1 === n2;
- });
- const chosen = exactMatch || gdata[0];
+ const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
+ const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
fetchedType = chosen ? chosen.type : null;
fetchedId = chosen ? chosen.id : null;
}
@@ -650,21 +660,20 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
const stopForStation = (stops || []).find(s => {
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
- const ns = normalizeName(nm || '');
- const target = normalizeName(targetStationName || '');
- return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
+ return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
}) || null;
- horaire._status = displayStatus;
- horaire._displayTime = displayTime;
- // utiliser quai en priorité
- horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay)) || horaire.quai || horaire.quai_reel || horaire.platform || '';
+ let platformVal = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || horaire.quai || horaire.quai_reel || horaire.platform || horaire.voie || '';
+ if (!platformVal) {
+ platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
+ }
+ horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
return horaire;
});
- }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
+ }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
useEffect(() => {
- // Alternance toutes les 4 secondes (type+numéro ↔ statut)
- const id = setInterval(() => setShowStatus(s => !s), 4000);
+ // Alternance toutes les 2 secondes (type+numéro ↔ statut / suppression)
+ const id = setInterval(() => setShowStatus(s => !s), 2000);
return () => clearInterval(id);
}, []);
@@ -707,11 +716,13 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
} else {
const formatted = (sncfData || []).map(t => {
const timeStr = t.heure_arrivee || t.heure_depart || t.time || '';
+ const arrHM = (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr);
+ const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
return {
...t,
_displayTime: formatTimeNoSeconds(timeStr),
- _displayPlatform: t.quai || t.voie || '',
- _arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr),
+ _displayPlatform: plat,
+ _arrHM: arrHM,
_status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '',
@@ -720,8 +731,10 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
}
};
});
- setHoraires(formatted);
- setAllHoraires(formatted);
+ const maxCount = defilementMode ? 20 : 10;
+ const limited = (formatted || []).slice(0, maxCount);
+ setHoraires(limited);
+ setAllHoraires(limited);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
@@ -791,15 +804,14 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
// Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
let displayHoraires = parsedHoraires.slice();
if (gareDisplay) {
- const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
displayHoraires = displayHoraires.filter(h => {
// Exclure si la gare ciblée est la gare de départ
- const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString());
- if (departName === normalized) return false;
+ const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString();
+ if (matchStationNameStrict(departName, gareDisplay)) return false;
// Inclure si gare d'arrivée correspond
- const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
- if (destName === normalized) return true;
+ const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
+ if (matchStationNameStrict(destName, gareDisplay)) return true;
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le point de départ (isFirst ou index==0)
const stopsList = getStops(h);
@@ -812,11 +824,11 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
}
}
for (const s of stopsList) {
- const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
+ const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString();
if (!stopName) continue;
const isOrigine = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && minIdx != null && Number(s.index) === minIdx));
if (isOrigine) continue;
- if (stopName === normalized) return true;
+ if (matchStationNameStrict(stopName, gareDisplay)) return true;
}
}
return false;
@@ -889,7 +901,7 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
const stopsListForH = getStops(h);
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null;
- const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null;
+ const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null;
const stop = byId || byName;
if (stop && (stop.heure_arrivee || stop.time || stop.heure)) raw = stop.heure_arrivee || stop.time || stop.heure;
}
@@ -1165,10 +1177,13 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
// Création des lignes vides pour créer l'espace
const emptyRows = Array.from({ length: 10 }).map((_, i) => ({ _isEmpty: true, id: `empty-${i}` }));
+ const maxRowsCount = defilementMode ? 20 : 10;
+ const limitedHoraires = (horaires || []).slice(0, maxRowsCount);
+
// Liste de lignes à rendre (horaires + espace + horaires + espace en mode défilement pour boucle infinie)
const rowsToRender = defilementMode
- ? [...horaires, ...emptyRows, ...horaires, ...emptyRows]
- : horaires;
+ ? [...limitedHoraires, ...emptyRows, ...limitedHoraires, ...emptyRows]
+ : limitedHoraires;
// Calcul dynamique de l'animation CSS
useEffect(() => {
@@ -1259,7 +1274,21 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
const isSuppressed = status && status.label === 'supprimé';
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
- const showPlatform = !isSuppressed && Boolean(cleanPlatform);
+ const isCar = (
+ (horaire.type_train && (horaire.type_train.toUpperCase().includes('AUTOCAR') || horaire.type_train.toUpperCase().includes('CAR TER') || horaire.type_train.toUpperCase() === 'BUS')) ||
+ (horaire.op && (horaire.op.toUpperCase().includes('AUTOCAR') || horaire.op.toUpperCase().includes('CAR TER') || horaire.op.toUpperCase() === 'BUS')) ||
+ (horaire.type && (horaire.type.toUpperCase().includes('AUTOCAR') || horaire.type.toUpperCase().includes('CAR TER') || horaire.type.toUpperCase() === 'BUS')) ||
+ Boolean(horaire.is_substitution) ||
+ (numTrain && String(numTrain).startsWith('3') && String(numTrain).length >= 5) ||
+ typeLabel === 'AUTOCAR' ||
+ rawPlatform === 'AUTOCAR' ||
+ rawPlatform === 'CAR' ||
+ rawPlatform === 'BUS' ||
+ cleanPlatform === 'AUTOCAR' ||
+ cleanPlatform === 'CAR' ||
+ cleanPlatform === 'BUS'
+ );
+ const showPlatform = !isSuppressed && (isCar || Boolean(cleanPlatform));
const stopsList = isExtended ? getStopsListForArrivees(horaire, gareDisplay || gareParam, normalizeName) : [];
@@ -1280,7 +1309,10 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
isSuppressed ? (
supprimé
) : isDelayed ? (
- retard {status.delayMinutes} min.
+
+
retardé
+
+{status.delayMinutes || horaire.retard_minutes || 0} min.
+
) : (
à l'heure
)
@@ -1293,7 +1325,12 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
@@ -1303,7 +1340,11 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
{showPlatform ? (
-
{cleanPlatform}
+ {isCar ? (
+
+ ) : (
+
{cleanPlatform}
+ )}
) : null}
@@ -1346,7 +1387,10 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
isSuppressed ? (
supprimé
) : isDelayed ? (
-
retard {status.delayMinutes} min.
+
+
retardé
+
+{status.delayMinutes || horaire.retard_minutes || 0} min.
+
) : (
à l'heure
)
@@ -1360,7 +1404,12 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
{/* COL 3 : Heure (grande, jaune) */}
{/* COL 4 : Provenance */}
@@ -1372,7 +1421,11 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
{showPlatform ? (
-
{cleanPlatform}
+ {isCar ? (
+
+ ) : (
+
{cleanPlatform}
+ )}
) : null}
diff --git a/src/pages/afficheurs/classiques/ClassiquesDeparts.css b/src/pages/afficheurs/classiques/ClassiquesDeparts.css
index e72b6b7..3227e3f 100644
--- a/src/pages/afficheurs/classiques/ClassiquesDeparts.css
+++ b/src/pages/afficheurs/classiques/ClassiquesDeparts.css
@@ -248,11 +248,25 @@
.aff-classiques-departs__status-delayed {
color: var(--yellow);
- font-size: 30px;
- font-weight: 700;
- line-height: 1.1;
- white-space: nowrap;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
font-family: "Achemine", sans-serif;
+ line-height: 1.05;
+}
+
+.aff-classiques-departs__status-delayed-label {
+ font-size: 24px;
+ font-weight: 700;
+ color: var(--yellow);
+ text-transform: lowercase;
+}
+
+.aff-classiques-departs__status-delayed-time {
+ font-size: 26px;
+ font-weight: 700;
+ color: var(--yellow);
}
.aff-classiques-departs__status-suppressed {
diff --git a/src/pages/afficheurs/classiques/ClassiquesDeparts.js b/src/pages/afficheurs/classiques/ClassiquesDeparts.js
index 9101d99..ebf1fd0 100644
--- a/src/pages/afficheurs/classiques/ClassiquesDeparts.js
+++ b/src/pages/afficheurs/classiques/ClassiquesDeparts.js
@@ -6,9 +6,23 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { fetchAllRows } from '../../../lib/fetchAllRows';
import { getSourceFromPath } from '../../../services/railwayService';
-import { getSncfTimetable } from '../../../services/sncfApi';
+import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi';
+import { matchStationNameStrict } from '../../../services/gtfsStations';
import { getTrainLogoAndMeta } from '../../../utils/trainLogoHelper';
+// Pictogramme Autocar SNCF / MOBIGO
+const AutocarIcon = ({ size = '50px', color = '#ffffff', style = {} }) => (
+
+);
+
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => {
if (v == null) return '';
@@ -212,12 +226,8 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
if (!gerr && gdata && gdata.length) {
- const exactMatch = gdata.find(g => {
- const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
- const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
- return n1 === n2;
- });
- const chosen = exactMatch || gdata[0];
+ const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
+ const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
fetchedType = chosen ? chosen.type : null;
fetchedId = chosen ? chosen.id : null;
}
@@ -650,21 +660,20 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
const stopForStation = (stops || []).find(s => {
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
- const ns = normalizeName(nm || '');
- const target = normalizeName(targetStationName || '');
- return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
+ return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
}) || null;
- horaire._status = displayStatus;
- horaire._displayTime = displayTime;
- // utiliser quai en priorité
- horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay)) || horaire.quai || horaire.quai_reel || horaire.platform || '';
+ let platformVal = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || horaire.quai || horaire.quai_reel || horaire.platform || horaire.voie || '';
+ if (!platformVal) {
+ platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
+ }
+ horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
return horaire;
});
- }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
+ }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
useEffect(() => {
- // Alternance toutes les 4 secondes (type+numéro ↔ statut)
- const id = setInterval(() => setShowStatus(s => !s), 4000);
+ // Alternance toutes les 2 secondes (type+numéro ↔ statut / suppression)
+ const id = setInterval(() => setShowStatus(s => !s), 2000);
return () => clearInterval(id);
}, []);
@@ -705,20 +714,26 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
if (sncfErr) {
setError(sncfErr);
} else {
- const formatted = (sncfData || []).map(t => ({
- ...t,
- _displayTime: formatTimeNoSeconds(t.heure_depart),
- _displayPlatform: t.quai || t.voie || '',
- _depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart),
- _status: t._status || {
- label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
- detail: '',
- delayMinutes: t.retard_minutes || null,
- causeDetails: t.cause_retard || null
- }
- }));
- setHoraires(formatted);
- setAllHoraires(formatted);
+ const formatted = (sncfData || []).map(t => {
+ const depHM = (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart);
+ const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
+ return {
+ ...t,
+ _displayTime: formatTimeNoSeconds(t.heure_depart),
+ _displayPlatform: plat,
+ _depHM: depHM,
+ _status: t._status || {
+ label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
+ detail: '',
+ delayMinutes: t.retard_minutes || null,
+ causeDetails: t.cause_retard || null
+ }
+ };
+ });
+ const maxCount = defilementMode ? 20 : 10;
+ const limited = (formatted || []).slice(0, maxCount);
+ setHoraires(limited);
+ setAllHoraires(limited);
}
} catch (e) {
if (mounted) setError(e.message || String(e));
@@ -788,15 +803,14 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
// Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
let displayHoraires = parsedHoraires.slice();
if (gareDisplay) {
- const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
displayHoraires = displayHoraires.filter(h => {
// Exclure si la gare ciblée est la gare d'arrivée finale
- const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
- if (destName === normalized) return false;
+ const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
+ if (matchStationNameStrict(destName, gareDisplay)) return false;
// Inclure si gare de départ correspond
- const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString());
- if (departName === normalized) return true;
+ const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString();
+ if (matchStationNameStrict(departName, gareDisplay)) return true;
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max)
const stopsList = getStops(h);
@@ -809,11 +823,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
}
}
for (const s of stopsList) {
- const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
+ const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString();
if (!stopName) continue;
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
if (isTerminus) continue;
- if (stopName === normalized) return true;
+ if (matchStationNameStrict(stopName, gareDisplay)) return true;
}
}
return false;
@@ -886,7 +900,7 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
const stopsListForH = getStops(h);
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null;
- const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null;
+ const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null;
const stop = byId || byName;
if (stop && (stop.heure_depart || stop.time || stop.heure)) raw = stop.heure_depart || stop.time || stop.heure;
}
@@ -1162,10 +1176,13 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
// Création des lignes vides pour créer l'espace
const emptyRows = Array.from({ length: 10 }).map((_, i) => ({ _isEmpty: true, id: `empty-${i}` }));
+ const maxRowsCount = defilementMode ? 20 : 10;
+ const limitedHoraires = (horaires || []).slice(0, maxRowsCount);
+
// Liste de lignes à rendre (horaires + espace + horaires + espace en mode défilement pour boucle infinie)
const rowsToRender = defilementMode
- ? [...horaires, ...emptyRows, ...horaires, ...emptyRows]
- : horaires;
+ ? [...limitedHoraires, ...emptyRows, ...limitedHoraires, ...emptyRows]
+ : limitedHoraires;
// Calcul dynamique de l'animation CSS
useEffect(() => {
@@ -1256,7 +1273,21 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
const isSuppressed = status && status.label === 'supprimé';
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
- const showPlatform = !isSuppressed && Boolean(cleanPlatform);
+ const isCar = (
+ (horaire.type_train && (horaire.type_train.toUpperCase().includes('AUTOCAR') || horaire.type_train.toUpperCase().includes('CAR TER') || horaire.type_train.toUpperCase() === 'BUS')) ||
+ (horaire.op && (horaire.op.toUpperCase().includes('AUTOCAR') || horaire.op.toUpperCase().includes('CAR TER') || horaire.op.toUpperCase() === 'BUS')) ||
+ (horaire.type && (horaire.type.toUpperCase().includes('AUTOCAR') || horaire.type.toUpperCase().includes('CAR TER') || horaire.type.toUpperCase() === 'BUS')) ||
+ Boolean(horaire.is_substitution) ||
+ (numTrain && String(numTrain).startsWith('3') && String(numTrain).length >= 5) ||
+ typeLabel === 'AUTOCAR' ||
+ rawPlatform === 'AUTOCAR' ||
+ rawPlatform === 'CAR' ||
+ rawPlatform === 'BUS' ||
+ cleanPlatform === 'AUTOCAR' ||
+ cleanPlatform === 'CAR' ||
+ cleanPlatform === 'BUS'
+ );
+ const showPlatform = !isSuppressed && (isCar || Boolean(cleanPlatform));
const stopsList = isExtended ? getStopsListForDeparts(horaire, gareDisplay || gareParam, normalizeName) : [];
@@ -1277,7 +1308,10 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
isSuppressed ? (
supprimé
) : isDelayed ? (
-
retard {status.delayMinutes} min.
+
+
retardé
+
+{status.delayMinutes || horaire.retard_minutes || 0} min.
+
) : (
à l'heure
)
@@ -1290,7 +1324,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
@@ -1300,7 +1339,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
{showPlatform ? (
-
{cleanPlatform}
+ {isCar ? (
+
+ ) : (
+
{cleanPlatform}
+ )}
) : null}
@@ -1343,7 +1386,10 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
isSuppressed ? (
supprimé
) : isDelayed ? (
-
retard {status.delayMinutes} min.
+
+
retardé
+
+{status.delayMinutes || horaire.retard_minutes || 0} min.
+
) : (
à l'heure
)
@@ -1357,7 +1403,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
{/* COL 3 : Heure (grande, jaune) */}
{/* COL 4 : Destination */}
@@ -1369,7 +1420,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
{showPlatform ? (
-
{cleanPlatform}
+ {isCar ? (
+
+ ) : (
+
{cleanPlatform}
+ )}
) : null}
diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.css b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.css
index 5c3baf6..7bde19b 100644
--- a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.css
+++ b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.css
@@ -37,6 +37,11 @@ body, html {
z-index: 2;
}
+.aff-classique-quai-arr__left--tgv {
+ bottom: 240px !important;
+ padding: 32px 48px 20px 48px !important;
+}
+
/* Logo Dynamique (TER Bleu / Opérateur) */
.aff-classique-quai-arr__brand {
display: flex;
@@ -65,6 +70,7 @@ body, html {
.aff-classique-quai-arr__time-row {
display: flex;
align-items: baseline;
+ min-height: 100px;
margin-bottom: 8px;
z-index: 3;
}
@@ -85,12 +91,18 @@ body, html {
margin-left: 20px;
}
-.aff-classique-quai-arr__time--suppressed,
+.aff-classique-quai-arr__status-alt {
+ font-weight: 700;
+ font-size: 68px;
+ line-height: 1;
+ letter-spacing: -0.5px;
+ white-space: nowrap;
+}
+
.aff-classique-quai-arr__status--suppressed {
color: #d32f2f !important;
}
-.aff-classique-quai-arr__time--delayed,
.aff-classique-quai-arr__status--delayed {
color: #f5a623 !important;
}
@@ -107,6 +119,22 @@ body, html {
word-break: break-word;
}
+.aff-classique-quai-arr__destination--multi {
+ font-size: 52px !important;
+ margin-top: 6px !important;
+ margin-bottom: 4px !important;
+}
+
+/* Services PMR, Vélo, Bar */
+.aff-classique-quai-arr__services {
+ display: flex;
+ align-items: center;
+ gap: 18px;
+ margin-top: auto;
+ margin-bottom: 8px;
+ z-index: 4;
+}
+
/* Info Train (Type + Numéro) */
.aff-classique-quai-arr__train-info {
display: flex;
@@ -173,6 +201,10 @@ body, html {
flex-direction: column;
}
+.aff-classique-quai-arr__right--tgv {
+ bottom: 240px !important;
+}
+
.aff-classique-quai-arr__stops-wrapper {
position: relative;
width: 100%;
@@ -258,6 +290,74 @@ body, html {
font-size: 44px;
}
+/* =========================================================
+ FOURCHE / BIFURCATION DESSERTE TGV (Conforme Image 2)
+ ========================================================= */
+.aff-classique-quai-arr__fork-container {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+
+.aff-classique-quai-arr__fork-split-row {
+ position: relative;
+ display: flex;
+ align-items: flex-start;
+ min-height: 100px;
+ padding-left: 65px;
+ margin-top: 4px;
+}
+
+/* Rail courbe violet (#5c538f) */
+.aff-classique-quai-arr__fork-curve-rail {
+ position: absolute;
+ left: 20px;
+ top: -15px;
+ width: 125px;
+ height: 80px;
+ border-left: 28px solid #5c538f;
+ border-bottom: 28px solid #5c538f;
+ border-bottom-left-radius: 35px;
+ z-index: 1;
+ pointer-events: none;
+}
+
+/* Branche droite */
+.aff-classique-quai-arr__fork-branch-right {
+ display: flex;
+ flex-direction: column;
+ margin-left: 115px;
+ margin-top: 10px;
+ z-index: 3;
+}
+
+/* Libellé "voitures 11 à 18" */
+.aff-classique-quai-arr__fork-coaches-label {
+ color: #f5a623;
+ font-size: 22px;
+ font-weight: 700;
+ margin-bottom: 4px;
+ letter-spacing: 0.5px;
+}
+
+/* Boîte terminus branche droite */
+.aff-classique-quai-arr__fork-terminus-box {
+ background-color: #5c538f;
+ padding: 8px 30px 8px 20px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.aff-classique-quai-arr__fork-terminus-name {
+ color: #ffffff;
+ font-size: 44px;
+ font-weight: 700;
+ white-space: nowrap;
+}
+
/* =========================================================
FOOTER & HORLOGE (Conforme Image 2)
========================================================= */
diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js
index 35ba995..a62c67e 100644
--- a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js
+++ b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js
@@ -6,10 +6,34 @@ import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../../services/railwayService';
import { getSncfTimetable } from '../../../../services/sncfApi';
+import { matchStationNameStrict } from '../../../../services/gtfsStations';
import GareVideoprotection from '../../../../components/afficheurs/GareVideoprotection';
import AttentifsEnsemble from '../../../../components/afficheurs/AttentifsEnsemble';
import ClassiquesInformations from '../ClassiquesInformations';
import ClassiquesArrivees from '../ClassiquesArrivees';
+import TgvCompositionQuai from '../../../../components/afficheurs/TgvCompositionQuai';
+
+function TgvServiceIcons() {
+ return (
+
+ {/* PMR / Fauteuil roulant */}
+
+ {/* Vélo */}
+
+ {/* Bar / Verre */}
+
+
+ );
+}
function DigitalClock() {
const [now, setNow] = useState(new Date());
@@ -131,6 +155,7 @@ export default function ClassiquesArriveesQuais() {
const [, setHoraireError] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date());
const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s)
+ const [showStatusAlt, setShowStatusAlt] = useState(false);
const pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase();
const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true';
@@ -144,6 +169,12 @@ export default function ClassiquesArriveesQuais() {
return () => clearInterval(id);
}, []);
+ // Alternance 3 secondes : Heure normale ↔ Statut (pour trains retardés ou supprimés)
+ useEffect(() => {
+ const id = setInterval(() => setShowStatusAlt(prev => !prev), 3000);
+ return () => clearInterval(id);
+ }, []);
+
// Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai
useEffect(() => {
if (selectedHoraire) return;
@@ -315,14 +346,29 @@ export default function ClassiquesArriveesQuais() {
if (sncfData && sncfData.length > 0) {
let matchingTrains = sncfData;
if (targetQuaiClean) {
- matchingTrains = sncfData.filter(t => cleanPlatform(t.quai || t.voie || t.quai_reel || t._displayPlatform) === targetQuaiClean);
+ matchingTrains = sncfData.filter(t => cleanPlatform(t._displayPlatform || t.quai || t.voie || t.quai_reel) === targetQuaiClean);
}
const train = matchingTrains[0];
if (train) {
const arrHM = parseTimeHM(train.heure_arrivee || train.time || train.heure);
+ const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || '';
+ const trainNumClean = (train.numero_train || train.num || '').toString();
+
+ // Recherche d'une deuxième tranche / train couplé au même horaire
+ const partner = matchingTrains.find(t => {
+ if (!t || t === train) return false;
+ const tNum = (t.numero_train || t.num || '').toString();
+ const tHM = parseTimeHM(t.heure_arrivee || t.time || t.heure);
+ return tNum !== trainNumClean && tHM === arrHM;
+ });
+
chosenTrain = {
horaire: {
...train,
+ quai: trainPlatform,
+ quai_reel: trainPlatform,
+ voie: trainPlatform,
+ _displayPlatform: trainPlatform,
_status: train._status || {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
detail: '',
@@ -330,10 +376,11 @@ export default function ClassiquesArriveesQuais() {
causeDetails: train.cause_retard || null
}
},
+ partner: partner || null,
stop: {
gare_nom: train.gare_arrivee_nom || gareDisplay || gareParam,
heure_arrivee: train.heure_arrivee || train.time || train.heure,
- quai: train.quai || train.voie || quaiParam
+ quai: trainPlatform
},
arrHM: arrHM != null ? arrHM : 0,
rawTime: train.heure_arrivee || train.time || train.heure
@@ -353,13 +400,14 @@ export default function ClassiquesArriveesQuais() {
} else if (gareDisplay) {
const { data: gdata } = await supabase.from('gares').select('id,nom').ilike('nom', `%${gareDisplay}%`);
if (gdata && gdata.length) {
- setResolvedGareId(gdata[0].id);
+ const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
+ const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
+ if (chosen) setResolvedGareId(chosen.id);
}
}
const { data: all, error } = await fetchAllRows(supabase.from('horaires').select('*'));
if (!error && Array.isArray(all) && all.length > 0) {
- const normalizedTarget = gareDisplay ? normalizeName(gareDisplay) : null;
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(now.getDate() + 1);
@@ -375,14 +423,14 @@ export default function ClassiquesArriveesQuais() {
const stops = getStops(h);
let stopMatch = null;
- if (normalizedTarget) {
- const departNameNorm = normalizeName(h.gare_depart_nom || h.gare_depart || '');
- if (departNameNorm && departNameNorm === normalizedTarget) {
+ if (gareDisplay) {
+ const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
+ if (matchStationNameStrict(departName, gareDisplay)) {
// Target is the departure origin -> not an arrival here
continue;
}
- const arriveeName = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || '');
- if (arriveeName && arriveeName === normalizedTarget) {
+ const arriveeName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
+ if (matchStationNameStrict(arriveeName, gareDisplay)) {
stopMatch = {
heure_arrivee: h.heure_arrivee || h.time || h.heure,
gare_nom: h.gare_arrivee_nom || h.gare_arrivee,
@@ -391,8 +439,8 @@ export default function ClassiquesArriveesQuais() {
}
if (!stopMatch && Array.isArray(stops)) {
for (const s of stops) {
- const sName = normalizeName(typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''));
- if (sName === normalizedTarget) {
+ const sName = (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || '')).toString();
+ if (matchStationNameStrict(sName, gareDisplay)) {
stopMatch = s;
break;
}
@@ -408,7 +456,11 @@ export default function ClassiquesArriveesQuais() {
if (!stopMatch) continue;
- const platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || '';
+ const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_arrivee || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_arrivee || h.time || h.heure;
+ const arrHM = parseTimeHM(rawTime);
+ if (arrHM == null) continue;
+
+ let platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || '';
const platClean = cleanPlatform(platform);
if (targetQuaiClean) {
@@ -417,10 +469,6 @@ export default function ClassiquesArriveesQuais() {
}
}
- const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_arrivee || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_arrivee || h.time || h.heure;
- const arrHM = parseTimeHM(rawTime);
- if (arrHM == null) continue;
-
candidates.push({
horaire: h,
stop: stopMatch,
@@ -448,7 +496,16 @@ export default function ClassiquesArriveesQuais() {
: (filteredCandidates.length > 0 ? filteredCandidates.sort((a, b) => a.arrHM - b.arrHM)[0] : null));
if (chosen) {
- chosenTrain = chosen;
+ const partnerCand = filteredCandidates.find(c => {
+ if (!c || c === chosen) return false;
+ const cNum = (c.horaire.numero_train || c.horaire.num || '').toString();
+ const chosenNum = (chosen.horaire.numero_train || chosen.horaire.num || '').toString();
+ return cNum !== chosenNum && c.arrHM === chosen.arrHM;
+ });
+ chosenTrain = {
+ ...chosen,
+ partner: partnerCand ? partnerCand.horaire : null
+ };
}
}
} catch (err) {
@@ -501,8 +558,7 @@ export default function ClassiquesArriveesQuais() {
// Filtrer les arrêts jusqu'à la gare courante
if (gareDisplay && stopsList.length > 0) {
- const normCur = normalizeName(gareDisplay);
- const foundIdx = stopsList.findIndex(s => normalizeName(s) === normCur);
+ const foundIdx = stopsList.findIndex(s => matchStationNameStrict(s, gareDisplay));
if (foundIdx > 0) {
stopsList = stopsList.slice(0, foundIdx + 1);
}
@@ -510,9 +566,8 @@ export default function ClassiquesArriveesQuais() {
// Garantir que le terminus est présent en dernier
if (destinationName) {
- const destNorm = normalizeName(destinationName);
- const lastNorm = stopsList.length > 0 ? normalizeName(stopsList[stopsList.length - 1]) : '';
- if (lastNorm !== destNorm) {
+ const lastStop = stopsList.length > 0 ? stopsList[stopsList.length - 1] : '';
+ if (!matchStationNameStrict(lastStop, destinationName)) {
stopsList.push(destinationName);
}
}
@@ -545,10 +600,64 @@ export default function ClassiquesArriveesQuais() {
}
}
+ const partner = selectedHoraire?.partner;
+ const destinationName1 = destinationName;
+ const destinationName2 = partner ? (partner.gare_arrivee_nom || partner.gare_arrivee || partner.destination || partner.dest || '') : (horaire?.second_destination || horaire?.dest2 || horaire?.destination2 || (horaire?.tranches && horaire?.tranches[1]?.destination) || '');
+
+ const trainType1 = trainType;
+ const trainType2 = partner ? (partner.type_train || partner.train_type || partner.type || partner.op || 'TGV') : (horaire?.type_train2 || horaire?.train_type2 || (destinationName2 ? trainType : ''));
+
+ const trainNumber1 = trainNumber;
+ const trainNumber2 = partner ? (partner.numero_train || partner.numero || partner.num || '') : (horaire?.numero_train2 || horaire?.num2 || (horaire?.tranches && horaire?.tranches[1]?.num) || '');
+
+ const isDifferentDest = Boolean(destinationName1 && destinationName2 && !matchStationNameStrict(destinationName1, destinationName2));
+
+ // Récupération des arrêts de la deuxième tranche
+ let stopsList2 = [];
+ if (partner) {
+ const rawStops2 = getStops(partner);
+ if (Array.isArray(rawStops2) && rawStops2.length > 0) {
+ stopsList2 = rawStops2.map(s => (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''))).filter(Boolean);
+ }
+ if (gareDisplay && stopsList2.length > 0) {
+ const fIdx = stopsList2.findIndex(s => matchStationNameStrict(s, gareDisplay));
+ if (fIdx >= 0 && fIdx < stopsList2.length - 1) {
+ stopsList2 = stopsList2.slice(fIdx + 1);
+ }
+ }
+ if (destinationName2 && !matchStationNameStrict(stopsList2[stopsList2.length - 1] || '', destinationName2)) {
+ stopsList2.push(destinationName2);
+ }
+ }
+
+ // Calcul du tronc commun et des branches de séparation (Fourche Image 2)
+ let commonStops = [];
+ let branch1Stops = [];
+ let branch2Stops = [];
+
+ if (isDifferentDest && stopsList.length > 0 && stopsList2.length > 0) {
+ let divIdx = 0;
+ const minLen = Math.min(stopsList.length, stopsList2.length);
+ while (divIdx < minLen && matchStationNameStrict(stopsList[divIdx], stopsList2[divIdx])) {
+ commonStops.push(stopsList[divIdx]);
+ divIdx++;
+ }
+ branch1Stops = stopsList.slice(divIdx);
+ branch2Stops = stopsList2.slice(divIdx);
+ }
+
+ const isTgv = Boolean(
+ (trainType1 || '').toUpperCase().includes('TGV') ||
+ (trainType2 || '').toUpperCase().includes('TGV') ||
+ (operator || '').toUpperCase().includes('TGV') ||
+ (brand && (brand.label || '').toUpperCase().includes('TGV')) ||
+ (horaire && ((horaire.type_train || horaire.type || '').toUpperCase().includes('TGV') || (horaire.op || '').toUpperCase().includes('TGV')))
+ );
+
return (
{/* PANNEAU GAUCHE : BLANC (Conforme Image 2) */}
-
+
{/* Logo Couleur Dynamique */}

@@ -559,68 +668,164 @@ export default function ClassiquesArriveesQuais() {
{/* Heure et Statut */}
-
- {formattedTime || '--:--'}
-
-
- {displayStatusText}
-
+ {(isSuppressed || isDelayed) ? (
+ showStatusAlt ? (
+
+ {displayStatusText}
+
+ ) : (
+
+ {formattedTime || '--:--'}
+
+ )
+ ) : (
+ <>
+
+ {formattedTime || '--:--'}
+
+
+ {displayStatusText}
+
+ >
+ )}
{/* Destination / Gare d'Arrivée Principale */}
-
- {destinationName || 'Aucune arrivée prévue'}
+
+ {destinationName1 || 'Aucune arrivée prévue'}
- {/* Type de Train & Numéro */}
+ {/* Type de Train & Numéro 1 */}
- {trainType && {trainType}}
- {trainNumber && {trainNumber}}
+ {trainType1 && {trainType1}}
+ {trainNumber1 && {trainNumber1}}
+ {/* Destination 2 si multi-tranche / destinations différentes */}
+ {isDifferentDest && (
+ <>
+
+ {destinationName2}
+
+
+ {trainType2 && {trainType2}}
+ {trainNumber2 && {trainNumber2}}
+
+ >
+ )}
+
+ {/* Si même destination mais 2 numéros couplés */}
+ {!isDifferentDest && trainNumber2 && trainNumber2 !== trainNumber1 && (
+
+ {trainType2 && {trainType2}}
+ {trainNumber2}
+
+ )}
+
+ {/* Pictogrammes de services pour TGV (PMR, Vélo, Bar) */}
+ {isTgv &&
}
+
{/* Filigrane Vertical "arrivée" géant (orienté bas vers haut) */}
arrivée
- {/* PANNEAU DROIT : VERT FORÊT AVEC DESSERTE (Conforme Image 2) */}
-
+ {/* PANNEAU DROIT : VERT FORÊT AVEC DESSERTE (Conforme Image 2 & Fourche) */}
+
- {stopsList.length > 0 ? (
-
- {stopsList.map((stopName, idx) => {
- const isLast = idx === stopsList.length - 1;
- return (
- -
+ {isDifferentDest && commonStops.length > 0 ? (
+ /* AFFICHAGE FOURCHE / BIFURCATION */
+
+ {/* Tronc commun */}
+
+ {commonStops.map((stopName, idx) => (
+ -
-
- {stopName}
-
+ {stopName}
- );
- })}
-
- ) : (
-
- Veuillez consulter les écrans du hall de la gare.
+ ))}
+
+
+ {/* Ligne d'embranchement avec courbe */}
+
+
+
+
voitures 11 à 18
+
+
+
+ {branch2Stops[branch2Stops.length - 1] || destinationName2}
+
+
+
+
+
+ {/* Branche principale continuant vers terminus 1 */}
+ {branch1Stops.length > 0 && (
+
+ {branch1Stops.map((stopName, idx) => {
+ const isLast = idx === branch1Stops.length - 1;
+ return (
+ -
+
+ {stopName}
+
+ );
+ })}
+
+ )}
+ ) : (
+ /* AFFICHAGE STANDARD DESSERTE VERTICALE */
+ stopsList.length > 0 ? (
+
+ {stopsList.map((stopName, idx) => {
+ const isLast = idx === stopsList.length - 1;
+ return (
+ -
+
+
+ {stopName}
+
+
+ );
+ })}
+
+ ) : (
+
+ Veuillez consulter les écrans du hall de la gare.
+
+ )
)}
- {/* BANDEAU BAS : PLEINE LARGEUR & HORLOGE (Conforme Image 2) */}
-
-
- {quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai ? `Voie ${cleanPlatform(horaire.quai).toUpperCase()}` : '')}
+ {/* BANDEAU BAS : TGV COMPOSITION OU BANDEAU STANDARD */}
+ {isTgv ? (
+
+ ) : (
+
+
+ {quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai || horaire?.voie || horaire?._displayPlatform ? `Voie ${cleanPlatform(horaire.quai || horaire.voie || horaire._displayPlatform).toUpperCase()}` : '')}
+
+
+
+
-
-
-
-
+ )}
);
}
\ No newline at end of file
diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css
index 95a4a6e..378bda1 100644
--- a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css
+++ b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css
@@ -37,6 +37,11 @@ body, html {
z-index: 2;
}
+.aff-classique-quai-dep__left--tgv {
+ bottom: 240px !important;
+ padding: 32px 48px 20px 48px !important;
+}
+
/* Logo Dynamique */
.aff-classique-quai-dep__brand {
display: flex;
@@ -65,6 +70,7 @@ body, html {
.aff-classique-quai-dep__time-row {
display: flex;
align-items: baseline;
+ min-height: 100px;
margin-bottom: 8px;
z-index: 3;
}
@@ -85,12 +91,18 @@ body, html {
margin-left: 20px;
}
-.aff-classique-quai-dep__time--suppressed,
+.aff-classique-quai-dep__status-alt {
+ font-weight: 700;
+ font-size: 68px;
+ line-height: 1;
+ letter-spacing: -0.5px;
+ white-space: nowrap;
+}
+
.aff-classique-quai-dep__status--suppressed {
color: #d32f2f !important;
}
-.aff-classique-quai-dep__time--delayed,
.aff-classique-quai-dep__status--delayed {
color: #f5a623 !important;
}
@@ -107,6 +119,22 @@ body, html {
word-break: break-word;
}
+.aff-classique-quai-dep__destination--multi {
+ font-size: 52px !important;
+ margin-top: 6px !important;
+ margin-bottom: 4px !important;
+}
+
+/* Services PMR, Vélo, Bar */
+.aff-classique-quai-dep__services {
+ display: flex;
+ align-items: center;
+ gap: 18px;
+ margin-top: auto;
+ margin-bottom: 8px;
+ z-index: 4;
+}
+
/* Info Train (Type + Numéro) */
.aff-classique-quai-dep__train-info {
display: flex;
@@ -173,6 +201,10 @@ body, html {
flex-direction: column;
}
+.aff-classique-quai-dep__right--tgv {
+ bottom: 240px !important;
+}
+
.aff-classique-quai-dep__stops-wrapper {
position: relative;
width: 100%;
@@ -258,6 +290,74 @@ body, html {
font-size: 44px;
}
+/* =========================================================
+ FOURCHE / BIFURCATION DESSERTE TGV (Conforme Image 2)
+ ========================================================= */
+.aff-classique-quai-dep__fork-container {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+}
+
+.aff-classique-quai-dep__fork-split-row {
+ position: relative;
+ display: flex;
+ align-items: flex-start;
+ min-height: 100px;
+ padding-left: 65px;
+ margin-top: 4px;
+}
+
+/* Rail courbe violet (#5c538f) */
+.aff-classique-quai-dep__fork-curve-rail {
+ position: absolute;
+ left: 20px;
+ top: -15px;
+ width: 125px;
+ height: 80px;
+ border-left: 28px solid #5c538f;
+ border-bottom: 28px solid #5c538f;
+ border-bottom-left-radius: 35px;
+ z-index: 1;
+ pointer-events: none;
+}
+
+/* Branche droite */
+.aff-classique-quai-dep__fork-branch-right {
+ display: flex;
+ flex-direction: column;
+ margin-left: 115px;
+ margin-top: 10px;
+ z-index: 3;
+}
+
+/* Libellé "voitures 11 à 18" */
+.aff-classique-quai-dep__fork-coaches-label {
+ color: #f5a623;
+ font-size: 22px;
+ font-weight: 700;
+ margin-bottom: 4px;
+ letter-spacing: 0.5px;
+}
+
+/* Boîte terminus branche droite (ex: Lille Europe) */
+.aff-classique-quai-dep__fork-terminus-box {
+ background-color: #5c538f;
+ padding: 8px 30px 8px 20px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.aff-classique-quai-dep__fork-terminus-name {
+ color: #ffffff;
+ font-size: 44px;
+ font-weight: 700;
+ white-space: nowrap;
+}
+
/* =========================================================
FOOTER & HORLOGE (Conforme Image 1)
========================================================= */
diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js
index a844c01..17f1108 100644
--- a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js
+++ b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js
@@ -6,11 +6,35 @@ import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../../services/railwayService';
import { getSncfTimetable } from '../../../../services/sncfApi';
+import { matchStationNameStrict } from '../../../../services/gtfsStations';
import GareVideoprotection from '../../../../components/afficheurs/GareVideoprotection';
import AttentifsEnsemble from '../../../../components/afficheurs/AttentifsEnsemble';
import DepartEnCoursClassique from '../../../../components/afficheurs/DepartEnCoursClassique';
import ClassiquesInformations from '../ClassiquesInformations';
import ClassiquesDeparts from '../ClassiquesDeparts';
+import TgvCompositionQuai from '../../../../components/afficheurs/TgvCompositionQuai';
+
+function TgvServiceIcons() {
+ return (
+
+ {/* PMR / Fauteuil roulant */}
+
+ {/* Vélo */}
+
+ {/* Bar / Verre */}
+
+
+ );
+}
function DigitalClock() {
const [now, setNow] = useState(new Date());
@@ -132,6 +156,7 @@ export default function ClassiquesDepartsQuais() {
const [, setHoraireError] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date());
const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s)
+ const [showStatusAlt, setShowStatusAlt] = useState(false);
const pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase();
const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true';
@@ -147,6 +172,12 @@ export default function ClassiquesDepartsQuais() {
return () => clearInterval(id);
}, []);
+ // Alternance 3 secondes : Heure normale ↔ Statut (pour trains retardés ou supprimés)
+ useEffect(() => {
+ const id = setInterval(() => setShowStatusAlt(prev => !prev), 3000);
+ return () => clearInterval(id);
+ }, []);
+
// Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai
useEffect(() => {
if (selectedHoraire) return;
@@ -317,14 +348,29 @@ export default function ClassiquesDepartsQuais() {
if (sncfData && sncfData.length > 0) {
let matchingTrains = sncfData;
if (targetQuaiClean) {
- matchingTrains = sncfData.filter(t => cleanPlatform(t.quai || t.voie || t.quai_reel || t._displayPlatform) === targetQuaiClean);
+ matchingTrains = sncfData.filter(t => cleanPlatform(t._displayPlatform || t.quai || t.voie || t.quai_reel) === targetQuaiClean);
}
const train = matchingTrains[0];
if (train) {
const depHM = parseTimeHM(train.heure_depart || train.time || train.heure);
+ const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || '';
+ const trainNumClean = (train.numero_train || train.num || '').toString();
+
+ // Recherche d'une deuxième tranche / train couplé au même horaire
+ const partner = matchingTrains.find(t => {
+ if (!t || t === train) return false;
+ const tNum = (t.numero_train || t.num || '').toString();
+ const tHM = parseTimeHM(t.heure_depart || t.time || t.heure);
+ return tNum !== trainNumClean && tHM === depHM;
+ });
+
chosenTrain = {
horaire: {
...train,
+ quai: trainPlatform,
+ quai_reel: trainPlatform,
+ voie: trainPlatform,
+ _displayPlatform: trainPlatform,
_status: train._status || {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
detail: '',
@@ -332,10 +378,11 @@ export default function ClassiquesDepartsQuais() {
causeDetails: train.cause_retard || null
}
},
+ partner: partner || null,
stop: {
gare_nom: train.gare_depart_nom || gareDisplay || gareParam,
heure_depart: train.heure_depart || train.time || train.heure,
- quai: train.quai || train.voie || quaiParam
+ quai: trainPlatform
},
depHM: depHM != null ? depHM : 0,
rawTime: train.heure_depart || train.time || train.heure
@@ -355,13 +402,14 @@ export default function ClassiquesDepartsQuais() {
} else if (gareDisplay) {
const { data: gdata } = await supabase.from('gares').select('id,nom').ilike('nom', `%${gareDisplay}%`);
if (gdata && gdata.length) {
- setResolvedGareId(gdata[0].id);
+ const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
+ const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
+ if (chosen) setResolvedGareId(chosen.id);
}
}
const { data: all, error } = await fetchAllRows(supabase.from('horaires').select('*'));
if (!error && Array.isArray(all) && all.length > 0) {
- const normalizedTarget = gareDisplay ? normalizeName(gareDisplay) : null;
const now = new Date();
const tomorrow = new Date(now);
tomorrow.setDate(now.getDate() + 1);
@@ -377,14 +425,14 @@ export default function ClassiquesDepartsQuais() {
const stops = getStops(h);
let stopMatch = null;
- if (normalizedTarget) {
- const destNameNorm = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || '');
- if (destNameNorm && destNameNorm === normalizedTarget) {
+ if (gareDisplay) {
+ const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
+ if (matchStationNameStrict(destName, gareDisplay)) {
// Destination finale -> terminus ici, pas un départ
continue;
}
- const departName = normalizeName(h.gare_depart_nom || h.gare_depart || '');
- if (departName && departName === normalizedTarget) {
+ const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
+ if (matchStationNameStrict(departName, gareDisplay)) {
stopMatch = {
heure_depart: h.heure_depart || h.time || h.heure,
gare_nom: h.gare_depart_nom || h.gare_depart,
@@ -393,8 +441,8 @@ export default function ClassiquesDepartsQuais() {
}
if (!stopMatch && Array.isArray(stops)) {
for (const s of stops) {
- const sName = normalizeName(typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''));
- if (sName === normalizedTarget) {
+ const sName = (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || '')).toString();
+ if (matchStationNameStrict(sName, gareDisplay)) {
stopMatch = s;
break;
}
@@ -410,7 +458,11 @@ export default function ClassiquesDepartsQuais() {
if (!stopMatch) continue;
- const platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || '';
+ const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_depart || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_depart || h.time || h.heure;
+ const depHM = parseTimeHM(rawTime);
+ if (depHM == null) continue;
+
+ let platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || '';
const platClean = cleanPlatform(platform);
if (targetQuaiClean) {
@@ -419,10 +471,6 @@ export default function ClassiquesDepartsQuais() {
}
}
- const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_depart || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_depart || h.time || h.heure;
- const depHM = parseTimeHM(rawTime);
- if (depHM == null) continue;
-
candidates.push({
horaire: h,
stop: stopMatch,
@@ -450,7 +498,16 @@ export default function ClassiquesDepartsQuais() {
: (filteredCandidates.length > 0 ? filteredCandidates.sort((a, b) => a.depHM - b.depHM)[0] : null));
if (chosen) {
- chosenTrain = chosen;
+ const partnerCand = filteredCandidates.find(c => {
+ if (!c || c === chosen) return false;
+ const cNum = (c.horaire.numero_train || c.horaire.num || '').toString();
+ const chosenNum = (chosen.horaire.numero_train || chosen.horaire.num || '').toString();
+ return cNum !== chosenNum && c.depHM === chosen.depHM;
+ });
+ chosenTrain = {
+ ...chosen,
+ partner: partnerCand ? partnerCand.horaire : null
+ };
}
}
} catch (err) {
@@ -503,8 +560,7 @@ export default function ClassiquesDepartsQuais() {
// Filtrer les arrêts à partir de la gare courante
if (gareDisplay && stopsList.length > 0) {
- const normCur = normalizeName(gareDisplay);
- const foundIdx = stopsList.findIndex(s => normalizeName(s) === normCur);
+ const foundIdx = stopsList.findIndex(s => matchStationNameStrict(s, gareDisplay));
if (foundIdx >= 0 && foundIdx < stopsList.length - 1) {
stopsList = stopsList.slice(foundIdx + 1);
}
@@ -512,9 +568,8 @@ export default function ClassiquesDepartsQuais() {
// Garantir que le terminus est présent en dernier
if (destinationName) {
- const destNorm = normalizeName(destinationName);
- const lastNorm = stopsList.length > 0 ? normalizeName(stopsList[stopsList.length - 1]) : '';
- if (lastNorm !== destNorm) {
+ const lastStop = stopsList.length > 0 ? stopsList[stopsList.length - 1] : '';
+ if (!matchStationNameStrict(lastStop, destinationName)) {
stopsList.push(destinationName);
}
}
@@ -567,10 +622,64 @@ export default function ClassiquesDepartsQuais() {
}
}
+ const partner = selectedHoraire?.partner;
+ const destinationName1 = destinationName;
+ const destinationName2 = partner ? (partner.gare_arrivee_nom || partner.gare_arrivee || partner.destination || partner.dest || '') : (horaire?.second_destination || horaire?.dest2 || horaire?.destination2 || (horaire?.tranches && horaire?.tranches[1]?.destination) || '');
+
+ const trainType1 = trainType;
+ const trainType2 = partner ? (partner.type_train || partner.train_type || partner.type || partner.op || 'TGV') : (horaire?.type_train2 || horaire?.train_type2 || (destinationName2 ? trainType : ''));
+
+ const trainNumber1 = trainNumber;
+ const trainNumber2 = partner ? (partner.numero_train || partner.numero || partner.num || '') : (horaire?.numero_train2 || horaire?.num2 || (horaire?.tranches && horaire?.tranches[1]?.num) || '');
+
+ const isDifferentDest = Boolean(destinationName1 && destinationName2 && !matchStationNameStrict(destinationName1, destinationName2));
+
+ // Récupération des arrêts de la deuxième tranche
+ let stopsList2 = [];
+ if (partner) {
+ const rawStops2 = getStops(partner);
+ if (Array.isArray(rawStops2) && rawStops2.length > 0) {
+ stopsList2 = rawStops2.map(s => (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''))).filter(Boolean);
+ }
+ if (gareDisplay && stopsList2.length > 0) {
+ const fIdx = stopsList2.findIndex(s => matchStationNameStrict(s, gareDisplay));
+ if (fIdx >= 0 && fIdx < stopsList2.length - 1) {
+ stopsList2 = stopsList2.slice(fIdx + 1);
+ }
+ }
+ if (destinationName2 && !matchStationNameStrict(stopsList2[stopsList2.length - 1] || '', destinationName2)) {
+ stopsList2.push(destinationName2);
+ }
+ }
+
+ // Calcul du tronc commun et des branches de séparation (Fourche Image 2)
+ let commonStops = [];
+ let branch1Stops = [];
+ let branch2Stops = [];
+
+ if (isDifferentDest && stopsList.length > 0 && stopsList2.length > 0) {
+ let divIdx = 0;
+ const minLen = Math.min(stopsList.length, stopsList2.length);
+ while (divIdx < minLen && matchStationNameStrict(stopsList[divIdx], stopsList2[divIdx])) {
+ commonStops.push(stopsList[divIdx]);
+ divIdx++;
+ }
+ branch1Stops = stopsList.slice(divIdx);
+ branch2Stops = stopsList2.slice(divIdx);
+ }
+
+ const isTgv = Boolean(
+ (trainType1 || '').toUpperCase().includes('TGV') ||
+ (trainType2 || '').toUpperCase().includes('TGV') ||
+ (operator || '').toUpperCase().includes('TGV') ||
+ (brand && (brand.label || '').toUpperCase().includes('TGV')) ||
+ (horaire && ((horaire.type_train || horaire.type || '').toUpperCase().includes('TGV') || (horaire.op || '').toUpperCase().includes('TGV')))
+ );
+
return (
- {/* PANNEAU GAUCHE : BLANC (Conforme Image 1) */}
-
+ {/* PANNEAU GAUCHE : BLANC (Conforme Image 1 & Image 2) */}
+
{/* Logo Couleur Dynamique */}

@@ -581,68 +690,164 @@ export default function ClassiquesDepartsQuais() {
{/* Heure et Statut */}
-
- {formattedTime || '--:--'}
-
-
- {displayStatusText}
-
+ {(isSuppressed || isDelayed) ? (
+ showStatusAlt ? (
+
+ {displayStatusText}
+
+ ) : (
+
+ {formattedTime || '--:--'}
+
+ )
+ ) : (
+ <>
+
+ {formattedTime || '--:--'}
+
+
+ {displayStatusText}
+
+ >
+ )}
{/* Destination Principale */}
-
- {destinationName || 'Aucun départ prévu'}
+
+ {destinationName1 || 'Aucun départ prévu'}
- {/* Type de Train & Numéro */}
+ {/* Type de Train & Numéro 1 */}
- {trainType && {trainType}}
- {trainNumber && {trainNumber}}
+ {trainType1 && {trainType1}}
+ {trainNumber1 && {trainNumber1}}
+ {/* Destination 2 si multi-tranche / destinations différentes (ex: Nice-Ville ou Bruxelles Midi) */}
+ {isDifferentDest && (
+ <>
+
+ {destinationName2}
+
+
+ {trainType2 && {trainType2}}
+ {trainNumber2 && {trainNumber2}}
+
+ >
+ )}
+
+ {/* Si même destination mais 2 numéros couplés (ex: 2 TGV départ 16h17 vers Paris) */}
+ {!isDifferentDest && trainNumber2 && trainNumber2 !== trainNumber1 && (
+
+ {trainType2 && {trainType2}}
+ {trainNumber2}
+
+ )}
+
+ {/* Pictogrammes de services pour TGV (PMR, Vélo, Bar) */}
+ {isTgv &&
}
+
{/* Filigrane Vertical "départ" géant (orienté bas vers haut) */}
départ
- {/* PANNEAU DROIT : BLEU NUIT AVEC DESSERTE (Conforme Image 1) */}
-
+ {/* PANNEAU DROIT : BLEU NUIT AVEC DESSERTE (Conforme Image 1 & Fourche Image 2) */}
+
- {stopsList.length > 0 ? (
-
- {stopsList.map((stopName, idx) => {
- const isLast = idx === stopsList.length - 1;
- return (
- -
+ {isDifferentDest && commonStops.length > 0 ? (
+ /* AFFICHAGE FOURCHE / BIFURCATION (Conforme Image 2) */
+
+ {/* Tronc commun */}
+
+ {commonStops.map((stopName, idx) => (
+ -
-
- {stopName}
-
+ {stopName}
- );
- })}
-
- ) : (
-
- Veuillez consulter les écrans du hall de la gare.
+ ))}
+
+
+ {/* Ligne d'embranchement avec courbe */}
+
+
+
+
voitures 11 à 18
+
+
+
+ {branch2Stops[branch2Stops.length - 1] || destinationName2}
+
+
+
+
+
+ {/* Branche principale continuant vers terminus 1 */}
+ {branch1Stops.length > 0 && (
+
+ {branch1Stops.map((stopName, idx) => {
+ const isLast = idx === branch1Stops.length - 1;
+ return (
+ -
+
+ {stopName}
+
+ );
+ })}
+
+ )}
+ ) : (
+ /* AFFICHAGE STANDARD DESSERTE VERTICALE (Conforme Image 1) */
+ stopsList.length > 0 ? (
+
+ {stopsList.map((stopName, idx) => {
+ const isLast = idx === stopsList.length - 1;
+ return (
+ -
+
+
+ {stopName}
+
+
+ );
+ })}
+
+ ) : (
+
+ Veuillez consulter les écrans du hall de la gare.
+
+ )
)}
- {/* BANDEAU BAS : PLEINE LARGEUR & HORLOGE (Conforme Image 1) */}
-
-
- {quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai ? `Voie ${cleanPlatform(horaire.quai).toUpperCase()}` : '')}
+ {/* BANDEAU BAS : TGV COMPOSITION OU BANDEAU STANDARD */}
+ {isTgv ? (
+
+ ) : (
+
+
+ {quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai || horaire?.voie || horaire?._displayPlatform ? `Voie ${cleanPlatform(horaire.quai || horaire.voie || horaire._displayPlatform).toUpperCase()}` : '')}
+
+
+
+
-
-
-
-
+ )}
);
}
\ No newline at end of file
diff --git a/src/services/gtfsStations.js b/src/services/gtfsStations.js
index c2bc916..c46e870 100644
--- a/src/services/gtfsStations.js
+++ b/src/services/gtfsStations.js
@@ -24569,6 +24569,7 @@ function cleanStationQuery(s) {
const STATION_ALIASES = {
'valence tgv': '87763029',
'valence': '87761007',
+ 'valence ville': '87761007',
'paris nord': '87271007',
'paris gare du nord': '87271007',
'paris est': '87113001',
@@ -24581,11 +24582,18 @@ const STATION_ALIASES = {
'austerlitz': '87547000',
'paris bercy': '87686667',
'bercy': '87686667',
+ 'paris saint lazare': '87384008',
+ 'saint lazare': '87384008',
'marseille': '87751008',
'marseille saint charles': '87751008',
'marseille st charles': '87751008',
+ 'marseille blancarde': '87751081',
'lyon part dieu': '87723197',
'lyon perrache': '87722025',
+ 'lyon jean mace': '87282624',
+ 'lyon gorge de loup': '87721175',
+ 'lyon vaise': '87721001',
+ 'lyon saint paul': '87721159',
'lyon saint exupery': '87762906',
'lyon st exupery': '87762906',
'toulouse': '87611004',
@@ -24614,9 +24622,73 @@ const STATION_ALIASES = {
'besancon franche comte tgv': '87300863',
'dijon': '87713040',
'dijon ville': '87713040',
+ 'dijon centre': '87713040',
+ 'dijon porte neuve': '87713073',
+ 'dijon porte-neuve': '87713073',
+ 'porte neuve': '87713073',
+ 'cannes': '87757625',
+ 'cannes la bocca': '87757617',
+ 'nice': '87756056',
+ 'nice ville': '87756056',
+ 'nice riquier': '87756353',
+ 'nice saint augustin': '87756254',
+ 'toulon': '87755009',
+ 'toulon sainte musse': '87742262',
'amiens': '87313874'
};
+/**
+ * Normalise un nom de gare pour comparaison stricte
+ */
+export function normalizeStation(name) {
+ if (!name) return '';
+ return String(name)
+ .toLowerCase()
+ .normalize('NFD')
+ .replace(/[\u0300-\u036f]/g, '')
+ .replace(/^gare\s+d['e]\s+/i, '')
+ .replace(/[-_/.,()'&]+/g, ' ')
+ .replace(/\bst\b/g, 'saint')
+ .replace(/\bste\b/g, 'sainte')
+ .replace(/\s+/g, ' ')
+ .trim();
+}
+
+/**
+ * Compare strictement deux noms de gares pour éviter la confusion entre
+ * gares de même ville (ex: "Dijon" vs "Dijon Porte Neuve", "Lyon Part Dieu" vs "Lyon Perrache")
+ */
+export function matchStationNameStrict(a, b) {
+ if (!a || !b) return false;
+ const normA = normalizeStation(a);
+ const normB = normalizeStation(b);
+ if (!normA || !normB) return false;
+ if (normA === normB) return true;
+
+ // Liste des qualificatifs de sous-gares / gares secondaires ou périphériques distinctes
+ const distinctQualifiers = [
+ 'porte neuve', 'part dieu', 'perrache', 'jean mace', 'gorge de loup', 'vaise',
+ 'saint paul', 'saint augustin', 'riquier', 'la bocca', 'bocca', 'pont du gard',
+ 'sud de france', 'sainte musse', 'europe', 'flandres', 'chateaucreux', 'bellevue',
+ 'carnot', 'terrasse', 'estressin', 'roche', 'franche comte tgv', 'montbeliard tgv'
+ ];
+
+ // Si l'une contient un qualificatif distinct et pas l'autre -> Ne correspondent PAS
+ for (const q of distinctQualifiers) {
+ const aHas = normA.includes(q);
+ const bHas = normB.includes(q);
+ if (aHas !== bHas) {
+ return false;
+ }
+ }
+
+ // Si on retire "ville" ou "centre", vérifions si elles sont identiques
+ const stripGeneric = (s) => s.replace(/\b(ville|centre|gare)\b/g, '').replace(/\s+/g, ' ').trim();
+ const sA = stripGeneric(normA);
+ const sB = stripGeneric(normB);
+ return sA === sB;
+}
+
export function findGtfsGare(query) {
if (!query) return null;
const rawQ = String(query).trim();
@@ -24633,7 +24705,11 @@ export function findGtfsGare(query) {
if (found) return found;
}
- // 3. Algorithme de scoring par tokens et similarité
+ // 3. Recherche par correspondance exacte nette
+ const exact = GTFS_GARES.find(g => cleanStationQuery(g.nom) === qClean);
+ if (exact) return exact;
+
+ // 4. Algorithme de scoring par tokens et similarité
const qTokens = qClean.split(' ').filter(Boolean);
const wantsTgv = qTokens.includes('tgv');
@@ -24649,8 +24725,12 @@ export function findGtfsGare(query) {
if (gClean === qClean) {
score = 1000;
- } else if (gClean.startsWith(qClean)) {
- score = 500;
+ } else if (matchStationNameStrict(g.nom, rawQ)) {
+ score = 900;
+ } else if (gClean.startsWith(qClean + ' ')) {
+ // Sous-gare ayant le préfixe mais plus longue (ex: "dijon porte neuve" quand la requête est "dijon")
+ // Pénaliser si l'utilisateur a tapé une requête courte exacte
+ score = 400 - (gTokens.length - qTokens.length) * 50;
} else {
let allFound = true;
let tokenMatches = 0;
@@ -24665,15 +24745,15 @@ export function findGtfsGare(query) {
}
if (allFound) {
- score = 200 + tokenMatches * 20 - (gTokens.length - qTokens.length) * 5;
+ score = 300 + tokenMatches * 20 - (gTokens.length - qTokens.length) * 40;
} else if (tokenMatches > 0) {
- score = tokenMatches * 15 - (gTokens.length - tokenMatches) * 5;
+ score = tokenMatches * 15 - (gTokens.length - tokenMatches) * 30;
}
}
if (wantsTgv && hasTgv) score += 150;
if (wantsTgv && !hasTgv) score -= 200;
- if (!wantsTgv && hasTgv) score -= 50;
+ if (!wantsTgv && hasTgv) score -= 100;
if (!wantsTgv && gClean.includes('ville') && qTokens.length === 1 && gTokens[0] === qTokens[0]) {
score += 100;
@@ -24707,6 +24787,8 @@ export function searchGtfsGares(query, limit = 20) {
if (gClean === qClean) {
scored.push({ gare: g, score: 1000 });
+ } else if (matchStationNameStrict(g.nom, query)) {
+ scored.push({ gare: g, score: 950 });
} else if (gClean.startsWith(qClean)) {
scored.push({ gare: g, score: 800 });
} else {
@@ -24718,9 +24800,9 @@ export function searchGtfsGares(query, limit = 20) {
}
}
if (matchCount === qTokens.length) {
- scored.push({ gare: g, score: 500 + matchCount * 10 - gTokens.length });
+ scored.push({ gare: g, score: 500 + matchCount * 10 - gTokens.length * 10 });
} else if (matchCount > 0) {
- scored.push({ gare: g, score: matchCount * 20 - gTokens.length });
+ scored.push({ gare: g, score: matchCount * 20 - gTokens.length * 10 });
}
}
}
diff --git a/src/services/sncfApi.js b/src/services/sncfApi.js
index db846a6..e663c6e 100644
--- a/src/services/sncfApi.js
+++ b/src/services/sncfApi.js
@@ -5,23 +5,81 @@ import { findGtfsGare, searchGtfsGares } from './gtfsStations';
const SNCF_GTFS_RT_TRIP_UPDATES = 'https://proxy.transport.data.gouv.fr/resource/sncf-gtfs-rt-trip-updates';
const SNCF_GTFS_RT_SERVICE_ALERTS = 'https://proxy.transport.data.gouv.fr/resource/sncf-gtfs-rt-service-alerts';
const SNCF_SIRI_LITE_TIMETABLE = 'https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-estimated-timetable';
+const SNCF_SIRI_LITE_SITUATION_EXCHANGE = 'https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-situation-exchange';
+const NAVITIA_BASE_URL = 'https://api.sncf.com/v1';
+const NAVITIA_TOKEN = (typeof process !== 'undefined' && process.env && process.env.REACT_APP_NAVITIA_TOKEN) || 'bf06df97-2ff1-4560-b74c-f127cb70764d';
-// Cache mémoire pour le flux GTFS-RT (TTL = 30s)
+// TTL de rafraîchissement des flux temps réel SNCF : 2 minutes (120 secondes)
+const REALTIME_CACHE_TTL = 120000;
+
+// Cache mémoire pour le flux GTFS-RT (TTL = 2 min)
let gtfsRtCache = {
timestamp: 0,
updates: new Map(),
alerts: []
};
-// Cache mémoire pour le flux SIRI-Lite (TTL = 30s)
+// Cache mémoire pour le flux SIRI-Lite Estimated Timetable (TTL = 2 min)
let siriLiteCache = {
timestamp: 0,
platforms: new Map(),
updates: new Map()
};
+// Cache mémoire pour le flux SIRI-Lite Situation Exchange (TTL = 2 min)
+let situationExchangeCache = {
+ timestamp: 0,
+ platforms: new Map(),
+ updates: new Map(),
+ alerts: []
+};
+
+// Cache mémoire pour l'API OpenData SNCF (TTL = 2 min)
+const openDataPlatformsCache = new Map();
+
+const MAJOR_HUBS = [
+ 'paris', 'lyon', 'dijon', 'marseille', 'lille', 'strasbourg', 'bordeaux',
+ 'toulouse', 'nantes', 'rennes', 'nice', 'montpellier', 'nimes', 'metz',
+ 'nancy', 'mulhouse', 'tours', 'le mans', 'rouen', 'clermont ferrand',
+ 'grenoble', 'chambery', 'avignon', 'valence', 'besancon viotte',
+ 'saint etienne', 'toulon', 'cannes', 'antibes', 'angers'
+];
+
+const MAJOR_HUB_UICS = [
+ '87713040', '87686006', '87271007', '87391003', '87113001', '87384008',
+ '87547000', '87686667', '87722025', '87723197', '87751008', '87286005',
+ '87286013', '87212027', '87581009', '87611004', '87481002', '87471003',
+ '87756056', '87773002', '87775007', '87192039', '87141002', '87182063',
+ '87571000', '87444000', '87411017', '87734004', '87747006', '87741009',
+ '87765008', '87318964', '87763029', '87718007', '87726000', '87755009',
+ '87757625', '87757674', '87484006'
+];
+
+export function isMajorHub(stationName, uic = '') {
+ if (uic && MAJOR_HUB_UICS.includes(String(uic).replace(/\D/g, ''))) return true;
+ if (!stationName) return false;
+ const clean = String(stationName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim();
+ return MAJOR_HUBS.some(hub => clean === hub || clean.startsWith(hub + ' ') || clean.endsWith(' ' + hub));
+}
+
+export function isCarNumber(num) {
+ const d = String(num || '').replace(/\D/g, '');
+ return (d.length === 5 && d.startsWith('3')) || (d.length === 6 && d.startsWith('3'));
+}
+
+/**
+ * Résout la voie pour une circulation :
+ * - Aucune voie nominale/fictive inventée : seuls les quais temps réel SNCF sont affichés.
+ * - Les services par autocar affichent 'AUTOCAR' (pictogramme).
+ */
+export function resolveNominalTrack(stationName, trainNum, uic = '') {
+ const rawNum = String(trainNum || '').replace(/\D/g, '');
+ if (isCarNumber(rawNum)) return 'AUTOCAR';
+ return '';
+}
+
// Helper fetch avec timeout pour ne jamais bloquer l'affichage en cas de réseau lent ou de blocage
-async function fetchWithTimeout(url, options = {}, timeoutMs = 7000) {
+async function fetchWithTimeout(url, options = {}, timeoutMs = 25000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);
try {
@@ -40,12 +98,53 @@ function cleanNum(n) {
return m ? m[0].replace(/^0+/, '') : String(n).trim();
}
+function extractAllTrainNumbers(journeyXml) {
+ const nums = new Set();
+
+ // 1. TrainNumberRef
+ const tnr = journeyXml.match(/
([^<]+)<\/TrainNumberRef>/);
+ if (tnr) {
+ const c = cleanNum(tnr[1]);
+ if (c) nums.add(c);
+ }
+
+ // 2. VehicleJourneyName
+ const vjn = journeyXml.match(/([^<]+)<\/VehicleJourneyName>/);
+ if (vjn) {
+ const c = cleanNum(vjn[1]);
+ if (c) nums.add(c);
+ }
+
+ // 3. PublishedLineName if numeric
+ const pln = journeyXml.match(/([^<]+)<\/PublishedLineName>/);
+ if (pln) {
+ const m = pln[1].match(/^\d+$/);
+ if (m) nums.add(cleanNum(m[0]));
+ }
+
+ // 4. DatedVehicleJourneyRef / VehicleJourneyRef / FramedVehicleJourneyRef
+ const refs = journeyXml.match(/<(?:DatedVehicleJourneyRef|VehicleJourneyRef|FramedVehicleJourneyRef)>([\s\S]*?)<\/(?:DatedVehicleJourneyRef|VehicleJourneyRef|FramedVehicleJourneyRef)>/g) || [];
+ for (const r of refs) {
+ const vjMatch = r.match(/FR:VehicleJourney::(\d{3,7})/i) || r.match(/VehicleJourney::(\d{3,7})/i) || r.match(/::(\d{3,7})/);
+ if (vjMatch) {
+ nums.add(cleanNum(vjMatch[1]));
+ }
+ const lrMatch = r.match(/LineRef::(\d{3,7})/i);
+ if (lrMatch) {
+ nums.add(cleanNum(lrMatch[1]));
+ }
+ }
+
+ return Array.from(nums);
+}
+
/**
- * Récupère et décode les flux temps réel SIRI Lite SNCF (Quais réels et horaires estimés)
+ * Récupère et décode le flux SIRI Lite SNCF officiel en temps réel (circulations dans les 60 prochaines minutes)
+ * Source : https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-estimated-timetable
*/
async function fetchSiriLiteData() {
const now = Date.now();
- if (now - siriLiteCache.timestamp < 30000 && siriLiteCache.platforms.size > 0) {
+ if (now - siriLiteCache.timestamp < REALTIME_CACHE_TTL && siriLiteCache.platforms.size > 0) {
return siriLiteCache;
}
@@ -53,79 +152,112 @@ async function fetchSiriLiteData() {
const updatesMap = new Map();
try {
- const res = await fetchWithTimeout(SNCF_SIRI_LITE_TIMETABLE, {}, 8000);
+ const res = await fetchWithTimeout(SNCF_SIRI_LITE_TIMETABLE, {
+ headers: {
+ 'Accept': 'application/xml, text/xml, */*'
+ }
+ }, 25000);
+
if (res && res.ok) {
const xml = await res.text();
const journeys = xml.split('');
for (let i = 1; i < journeys.length; i++) {
const j = journeys[i];
- const trainNumMatch = j.match(/([^<]+)<\/TrainNumberRef>/);
- const rawTrainNum = trainNumMatch ? trainNumMatch[1].trim() : '';
- const trainNum = cleanNum(rawTrainNum);
- const journeyRefMatch = j.match(/([^<]+)<\/DatedVehicleJourneyRef>/) || j.match(/([^<]+)<\/VehicleJourneyRef>/);
+ // 1. Extraction exhaustive des identifiants et numéros de train
+ const trainNums = extractAllTrainNumbers(j);
+
+ const journeyRefMatch = j.match(/([^<]+)<\/DatedVehicleJourneyRef>/) || j.match(/([^<]+)<\/VehicleJourneyRef>/) || j.match(/[\s\S]*?([^<]+)<\/DatedVehicleJourneyRef>/);
const journeyRef = journeyRefMatch ? journeyRefMatch[1].trim() : '';
- const isCanceled = j.includes('true');
+ const isCanceledJourney = j.includes('true') || j.includes('cancelled');
- // Extraire les arrêts et quais (RecordedCall et EstimatedCall)
- const callMatches = j.match(/<(RecordedCall|EstimatedCall)>[\s\S]*?<\/\1>/g) || [];
+ // 2. Extraire tous les arrêts desservis (RecordedCall et EstimatedCall)
+ const callMatches = j.match(/<(?:RecordedCall|EstimatedCall)>[\s\S]*?<\/(?:RecordedCall|EstimatedCall)>/g) || [];
for (const call of callMatches) {
- const stopRefMatch = call.match(/[^:]*::(\d+)<\/StopPointRef>/) || call.match(/([^<]+)<\/StopPointRef>/);
- const uicMatch = stopRefMatch ? (stopRefMatch[1].match(/\d{7,8}/) || [stopRefMatch[1]]) : null;
- const uic = uicMatch ? uicMatch[0] : '';
+ // Extraction du code UIC de l'arrêt
+ const stopRefMatch = call.match(/([^<]+)<\/StopPointRef>/);
+ let uic = '';
+ let uic7 = '';
+ if (stopRefMatch) {
+ const rawRef = stopRefMatch[1];
+ const uicDigits = rawRef.match(/\d{7,8}/);
+ uic = uicDigits ? uicDigits[0] : rawRef.replace(/\D/g, '');
+ if (uic.length === 8 && uic.startsWith('87')) {
+ uic7 = uic.slice(2);
+ }
+ }
const stopNameMatch = call.match(/([^<]+)<\/StopPointName>/);
const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
- const depPlatMatch = call.match(/([^<]+)<\/DeparturePlatformName>/);
- const arrPlatMatch = call.match(/([^<]+)<\/ArrivalPlatformName>/);
- const platform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : '')).trim();
+ // Extraction des quais (Départ, Arrivée, Prévu ou Réel)
+ const depPlatMatch = call.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName)>([^<]+)<\/(?:DeparturePlatformName|PlannedDeparturePlatformName)>/);
+ const arrPlatMatch = call.match(/<(?:ArrivalPlatformName|PlannedArrivalPlatformName)>([^<]+)<\/(?:ArrivalPlatformName|PlannedArrivalPlatformName)>/);
+ const quayMatch = call.match(/([^<]+)<\/QuayRef>/);
+ let rawPlatform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : (quayMatch ? quayMatch[1] : ''))).trim();
+ const platform = rawPlatform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
+
+ // Extraction des heures et calcul du retard
const expDepMatch = call.match(/([^<]+)<\/ExpectedDepartureTime>/);
const aimDepMatch = call.match(/([^<]+)<\/AimedDepartureTime>/);
+ const expArrMatch = call.match(/([^<]+)<\/ExpectedArrivalTime>/);
+ const aimArrMatch = call.match(/([^<]+)<\/AimedArrivalTime>/);
let delayMin = 0;
- if (expDepMatch && aimDepMatch) {
- const expD = new Date(expDepMatch[1]).getTime();
- const aimD = new Date(aimDepMatch[1]).getTime();
+ const expTimeStr = (expDepMatch && expDepMatch[1]) || (expArrMatch && expArrMatch[1]);
+ const aimTimeStr = (aimDepMatch && aimDepMatch[1]) || (aimArrMatch && aimArrMatch[1]);
+
+ if (expTimeStr && aimTimeStr) {
+ const expD = new Date(expTimeStr).getTime();
+ const aimD = new Date(aimTimeStr).getTime();
if (!isNaN(expD) && !isNaN(aimD)) {
delayMin = Math.max(0, Math.round((expD - aimD) / 60000));
}
}
+ const isStopCancelled = isCanceledJourney || call.includes('cancelled') || call.includes('cancelled') || call.includes('true');
+
if (platform) {
- if (uic) {
- if (trainNum) {
- platformsMap.set(`${trainNum}_${uic}`, platform);
- platformsMap.set(`${uic}_${trainNum}`, platform);
+ for (const tNum of trainNums) {
+ if (uic) {
+ platformsMap.set(`${tNum}_${uic}`, platform);
+ platformsMap.set(`${uic}_${tNum}`, platform);
}
- if (rawTrainNum) {
- platformsMap.set(`${rawTrainNum}_${uic}`, platform);
- platformsMap.set(`${uic}_${rawTrainNum}`, platform);
+ if (uic7) {
+ platformsMap.set(`${tNum}_${uic7}`, platform);
+ platformsMap.set(`${uic7}_${tNum}`, platform);
}
- if (journeyRef) {
+ if (stopNameClean) {
+ platformsMap.set(`${tNum}_${stopNameClean}`, platform);
+ }
+ }
+ if (journeyRef) {
+ if (uic) {
platformsMap.set(`${journeyRef}_${uic}`, platform);
platformsMap.set(`${uic}_${journeyRef}`, platform);
}
- }
- if (stopNameClean) {
- if (trainNum) platformsMap.set(`${trainNum}_${stopNameClean}`, platform);
- if (rawTrainNum) platformsMap.set(`${rawTrainNum}_${stopNameClean}`, platform);
- if (journeyRef) platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
+ if (stopNameClean) {
+ platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
+ }
}
}
- if (trainNum || rawTrainNum || journeyRef) {
- const info = {
- platform: platform,
- delayMinutes: delayMin,
- isCanceled: isCanceled
- };
- if (trainNum) updatesMap.set(trainNum, info);
- if (rawTrainNum) updatesMap.set(rawTrainNum, info);
- if (journeyRef) updatesMap.set(journeyRef, info);
+ const info = {
+ platform: platform,
+ delayMinutes: delayMin,
+ isCanceled: isStopCancelled
+ };
+
+ for (const tNum of trainNums) {
+ if (uic) updatesMap.set(`${tNum}_${uic}`, info);
+ updatesMap.set(tNum, info);
+ }
+ if (journeyRef) {
+ if (uic) updatesMap.set(`${journeyRef}_${uic}`, info);
+ updatesMap.set(journeyRef, info);
}
}
}
@@ -137,12 +269,298 @@ async function fetchSiriLiteData() {
updates: updatesMap
};
} catch (err) {
- console.warn('[sncfApi] Notice: SIRI-Lite refresh error/timeout:', err);
+ console.warn('[sncfApi] Notice: SIRI-Lite Estimated Timetable refresh error/timeout:', err);
}
return siriLiteCache;
}
+/**
+ * Récupère et décode le flux SIRI Lite Situation Exchange SNCF (Circulations à venir, perturbations, changements de voie, suppressions)
+ * Source : https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-situation-exchange
+ */
+async function fetchSituationExchangeData() {
+ const now = Date.now();
+ if (now - situationExchangeCache.timestamp < REALTIME_CACHE_TTL && situationExchangeCache.platforms.size > 0) {
+ return situationExchangeCache;
+ }
+
+ const platformsMap = new Map();
+ const updatesMap = new Map();
+ const alertsList = [];
+
+ try {
+ const res = await fetchWithTimeout(SNCF_SIRI_LITE_SITUATION_EXCHANGE, {
+ headers: {
+ 'Accept': 'application/xml, text/xml, */*'
+ }
+ }, 12000);
+
+ if (res && res.ok) {
+ const xml = await res.text();
+ const situations = xml.split('');
+
+ for (let i = 1; i < situations.length; i++) {
+ const s = situations[i];
+
+ // 1. Extraction exhaustive du train et de la mission
+ const trainNums = extractAllTrainNumbers(s);
+ const trainNumMatch = s.match(/([^<]+)<\/TrainNumberRef>/) || s.match(/([^<]+)<\/VehicleJourneyRef>/) || s.match(/([^<]+)<\/PublishedLineName>/);
+ const rawTrainNum = trainNumMatch ? trainNumMatch[1].trim() : '';
+ const primaryTrainNum = cleanNum(rawTrainNum);
+ if (primaryTrainNum && !trainNums.includes(primaryTrainNum)) {
+ trainNums.push(primaryTrainNum);
+ }
+
+ // 2. Extraction de l'arrêt impacté
+ const stopRefMatch = s.match(/([^<]+)<\/StopPointRef>/);
+ let uic = '';
+ let uic7 = '';
+ if (stopRefMatch) {
+ const rawRef = stopRefMatch[1];
+ const uicDigits = rawRef.match(/\d{7,8}/);
+ uic = uicDigits ? uicDigits[0] : rawRef.replace(/\D/g, '');
+ if (uic.length === 8 && uic.startsWith('87')) {
+ uic7 = uic.slice(2);
+ }
+ }
+
+ const stopNameMatch = s.match(/([^<]+)<\/StopPointName>/);
+ const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
+
+ // 3. Extraction des messages en français
+ const summaryFrMatch = s.match(/]*xml:lang="FR"[^>]*>([^<]+)<\/Summary>/i) || s.match(/]*>([^<]+)<\/Summary>/i);
+ const summary = summaryFrMatch ? summaryFrMatch[1].trim() : '';
+
+ const descFrMatch = s.match(/]*xml:lang="FR"[^>]*>([^<]+)<\/Description>/i) || s.match(/]*>([^<]+)<\/Description>/i);
+ const desc = descFrMatch ? descFrMatch[1].trim() : '';
+
+ const messageText = desc || summary || '';
+
+ // 4. Détection de changement de quai / voie
+ const platTagMatch = s.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName|ArrivalPlatformName|PlannedArrivalPlatformName|QuayRef)>([^<]+)<\//i);
+ let platform = platTagMatch ? platTagMatch[1].replace(/^(?:voie|quai|track|platform)\s*/i, '').trim() : '';
+
+ if (!platform && messageText) {
+ const voieTextMatch = messageText.match(/\b(?:voie|quai)\s+([A-Za-z0-9]+)\b/i);
+ if (voieTextMatch) {
+ platform = voieTextMatch[1].trim();
+ }
+ }
+
+ // 5. Détection de suppression
+ const isCanceled = /supprim|annul|gelöscht|cancelled|interrompu/i.test(messageText) || s.includes('true');
+
+ if (platform) {
+ for (const tNum of trainNums) {
+ if (uic) {
+ platformsMap.set(`${tNum}_${uic}`, platform);
+ platformsMap.set(`${uic}_${tNum}`, platform);
+ }
+ if (uic7) {
+ platformsMap.set(`${tNum}_${uic7}`, platform);
+ platformsMap.set(`${uic7}_${tNum}`, platform);
+ }
+ if (stopNameClean) {
+ platformsMap.set(`${tNum}_${stopNameClean}`, platform);
+ }
+ }
+ }
+
+ const info = {
+ platform: platform,
+ delayMinutes: 0,
+ isCanceled: isCanceled,
+ cause: summary || desc || ''
+ };
+
+ for (const tNum of trainNums) {
+ if (uic) updatesMap.set(`${tNum}_${uic}`, info);
+ updatesMap.set(tNum, info);
+ }
+
+ if (messageText) {
+ alertsList.push({
+ header: summary,
+ text: desc || summary
+ });
+ }
+ }
+ }
+
+ situationExchangeCache = {
+ timestamp: now,
+ platforms: platformsMap,
+ updates: updatesMap,
+ alerts: alertsList
+ };
+ } catch (err) {
+ console.warn('[sncfApi] Notice: SIRI-Lite Situation Exchange error/timeout:', err);
+ }
+
+ return situationExchangeCache;
+}
+
+/**
+ * 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 rawDt = (item.stop_date_time && (item.stop_date_time.departure_date_time || item.stop_date_time.arrival_date_time || item.stop_date_time.base_departure_date_time || item.stop_date_time.base_arrival_date_time)) || '';
+ let timeHM = '';
+ if (rawDt.includes('T')) {
+ const tPart = rawDt.split('T')[1];
+ if (tPart && tPart.length >= 4) {
+ timeHM = `${tPart.slice(0, 2)}:${tPart.slice(2, 4)}`;
+ }
+ }
+
+ 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;
+}
+
/**
* Récupère et décode les mises à jour temps réel GTFS-RT SNCF
*/
@@ -311,8 +729,8 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
const publicUrl = (typeof process !== 'undefined' && process.env && process.env.PUBLIC_URL) ? process.env.PUBLIC_URL : '';
const stationUrl = `${publicUrl}/data/stations/${uic}.json`;
- // Chargement parallèle tolérant aux pannes réseau / CORS
- const [jsonRes, rtData, siriData] = await Promise.all([
+ // Chargement parallèle tolérant aux pannes réseau / CORS (OpenData SNCF, SIRI-Lite Estimated Timetable, SIRI-Lite Situation Exchange, GTFS-RT)
+ const [jsonRes, rtData, siriData, sxData, openDataPlatforms] = await Promise.all([
fetch(stationUrl).catch(err => {
console.warn(`[sncfApi] Notice: fetch station ${uic} error:`, err);
return null;
@@ -322,8 +740,16 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
return { updates: new Map(), alerts: [] };
}),
fetchSiriLiteData().catch(err => {
- console.warn('[sncfApi] Notice: SIRI-Lite error:', err);
+ console.warn('[sncfApi] Notice: SIRI-Lite Timetable error:', err);
return { platforms: new Map(), updates: new Map() };
+ }),
+ fetchSituationExchangeData().catch(err => {
+ console.warn('[sncfApi] Notice: SIRI-Lite Situation Exchange error:', err);
+ return { platforms: new Map(), updates: new Map(), alerts: [] };
+ }),
+ fetchOpenDataSncfPlatforms(uic, mode).catch(err => {
+ console.warn('[sncfApi] Notice: OpenData SNCF platform error:', err);
+ return new Map();
})
]);
@@ -333,6 +759,8 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
}
const data = await jsonRes.json();
+ const gareRef = findGtfsGare(uic);
+ const targetStationNom = (gareRef && gareRef.nom) || (data && data.nom) || '';
const rawList = mode === 'arrivees' ? data.arrivees : data.departs;
if (!Array.isArray(rawList) || rawList.length === 0) return null;
@@ -362,21 +790,18 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
- try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
- }
- if (Array.isArray(customDays) && customDays.length > 0) {
- const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetYYYYMMDD);
- if (match) return Boolean(match.circule);
+ try { customDays = JSON.parse(customDaysRaw); } catch (_) { customDays = []; }
}
+ if (customDays.includes(targetDateHyphen)) return true;
}
- // 3. Colonnes circule_lundi, circule_mardi...
- const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
- const colName = `circule_${dayNames[targetDay]}`;
- if (typeof t[colName] !== 'undefined' && t[colName] !== null) {
- const db = t.date_debut ? String(t.date_debut).slice(0, 10) : null;
+ // 3. Base Supabase : circulation standard par jour de semaine
+ const daysMap = ['circule_dimanche', 'circule_lundi', 'circule_mardi', 'circule_mercredi', 'circule_jeudi', 'circule_vendredi', 'circule_samedi'];
+ const colName = daysMap[targetDay];
+ if (typeof t[colName] !== 'undefined') {
+ const dd = t.date_debut ? String(t.date_debut).slice(0, 10) : null;
const df = t.date_fin ? String(t.date_fin).slice(0, 10) : null;
- if (db && targetDateHyphen < db) return false;
+ if (dd && targetDateHyphen < dd) return false;
if (df && targetDateHyphen > df) return false;
return Boolean(t[colName]);
}
@@ -399,21 +824,37 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
return true;
};
- // Helper de résolution de quai
- const lookupPlatform = (trainNum, tripId, stationUic, stationName) => {
+ // Helper de résolution de quai en temps réel (Estimated Timetable + Situation Exchange + OpenData)
+ const lookupPlatform = (trainNum, tripId, stationUic, stationName, rawTimeStr = '', destName = '') => {
const cNum = cleanNum(trainNum);
+ const rawNum = String(trainNum || '').replace(/\D/g, '');
const cName = stationName ? formatStationName(stationName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
+ const cDest = destName ? formatStationName(destName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
+ const uicClean = String(stationUic || '').replace(/\D/g, '');
+ const uic7 = (uicClean.length === 8 && uicClean.startsWith('87')) ? uicClean.slice(2) : '';
return (
- (cNum && stationUic && siriData.platforms.get(`${cNum}_${stationUic}`)) ||
- (trainNum && stationUic && siriData.platforms.get(`${trainNum}_${stationUic}`)) ||
- (stationUic && cNum && siriData.platforms.get(`${stationUic}_${cNum}`)) ||
- (stationUic && trainNum && siriData.platforms.get(`${stationUic}_${trainNum}`)) ||
+ // 1. SIRI-Lite Estimated Timetable (60 prochaines min - Priorité 1)
+ (cNum && uicClean && siriData.platforms.get(`${cNum}_${uicClean}`)) ||
+ (rawNum && uicClean && siriData.platforms.get(`${rawNum}_${uicClean}`)) ||
+ (uicClean && cNum && siriData.platforms.get(`${uicClean}_${cNum}`)) ||
+ (cNum && uic7 && siriData.platforms.get(`${cNum}_${uic7}`)) ||
+ (uic7 && cNum && siriData.platforms.get(`${uic7}_${cNum}`)) ||
(cNum && cName && siriData.platforms.get(`${cNum}_${cName}`)) ||
- (trainNum && cName && siriData.platforms.get(`${trainNum}_${cName}`)) ||
- (tripId && stationUic && siriData.platforms.get(`${tripId}_${stationUic}`)) ||
- (stationUic && tripId && siriData.platforms.get(`${stationUic}_${tripId}`)) ||
+ (rawNum && cName && siriData.platforms.get(`${rawNum}_${cName}`)) ||
+ (tripId && uicClean && siriData.platforms.get(`${tripId}_${uicClean}`)) ||
(tripId && cName && siriData.platforms.get(`${tripId}_${cName}`)) ||
+ // 2. SIRI-Lite Situation Exchange (Circulations à venir / Changements de quai)
+ (cNum && uicClean && sxData.platforms.get(`${cNum}_${uicClean}`)) ||
+ (rawNum && uicClean && sxData.platforms.get(`${rawNum}_${uicClean}`)) ||
+ (uicClean && cNum && sxData.platforms.get(`${uicClean}_${cNum}`)) ||
+ (cNum && cName && sxData.platforms.get(`${cNum}_${cName}`)) ||
+ (rawNum && cName && sxData.platforms.get(`${rawNum}_${cName}`)) ||
+ // 3. OpenData SNCF direct
+ (cNum && uicClean && openDataPlatforms.get(`${cNum}_${uicClean}`)) ||
+ (rawNum && uicClean && openDataPlatforms.get(`${rawNum}_${uicClean}`)) ||
+ (cNum && cName && openDataPlatforms.get(`${cNum}_${cName}`)) ||
+ (cNum && cDest && openDataPlatforms.get(`${cNum}_${cDest}`)) ||
''
);
};
@@ -423,17 +864,30 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
const rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr);
const timeHM = parseTimeHM(rawTime) || 0;
- // Recherche de mise à jour GTFS-RT ou SIRI-Lite (valide pour aujourd'hui)
+ // Recherche de mise à jour GTFS-RT, SIRI-Lite Timetable ou SIRI-Lite Situation Exchange
const rt = !isTomorrow ? (rtData.updates.get(t.trip_id) || (t.num ? rtData.updates.get(t.num) : null) || (cleanNum(t.num) ? rtData.updates.get(cleanNum(t.num)) : null)) : null;
const siri = !isTomorrow ? (siriData.updates.get(t.num) || (cleanNum(t.num) ? siriData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? siriData.updates.get(t.trip_id) : null)) : null;
+ const sx = !isTomorrow ? (sxData.updates.get(t.num) || (cleanNum(t.num) ? sxData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? sxData.updates.get(t.trip_id) : null)) : null;
- const isCanceled = (rt && rt.isCanceled) || (siri && siri.isCanceled) || false;
+ const isCanceled = (rt && rt.isCanceled) || (siri && siri.isCanceled) || (sx && sx.isCanceled) || false;
const delayMinutes = (rt && rt.delayMinutes > 0) ? rt.delayMinutes : (siri && siri.delayMinutes > 0 ? siri.delayMinutes : 0);
- const currentStationName = mode === 'arrivees' ? t.dest : t.orig;
- let realVoie = lookupPlatform(t.num, t.trip_id, uic, currentStationName);
- if (!realVoie && t.quai && typeof t.quai === 'string') {
- realVoie = t.quai.trim();
+ const currentStationName = targetStationNom || (data && data.nom) || (mode === 'arrivees' ? t.dest : t.orig);
+ const destinationStation = mode === 'arrivees' ? t.orig : t.dest;
+ const isAutocar = (
+ (t.type && (t.type.toUpperCase().includes('AUTOCAR') || t.type.toUpperCase().includes('CAR') || t.type.toUpperCase() === 'BUS')) ||
+ (t.type_train && (t.type_train.toUpperCase().includes('AUTOCAR') || t.type_train.toUpperCase().includes('CAR') || t.type_train.toUpperCase() === 'BUS')) ||
+ (t.op && (t.op.toUpperCase().includes('AUTOCAR') || t.op.toUpperCase().includes('CAR') || t.op.toUpperCase() === 'BUS')) ||
+ Boolean(t.is_substitution) ||
+ isCarNumber(t.num || t.numero_train || t.numero)
+ );
+
+ let realVoie = lookupPlatform(t.num, t.trip_id, uic, currentStationName, rawTime, destinationStation);
+ if (!realVoie && (t.quai || t.quai_reel || t.platform || t.voie)) {
+ realVoie = String(t.quai || t.quai_reel || t.platform || t.voie).replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
+ }
+ if (!realVoie) {
+ realVoie = isAutocar ? 'AUTOCAR' : resolveNominalTrack(currentStationName, t.num, uic);
}
let statusLabel = "à l'heure";
@@ -446,14 +900,24 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
etat = 'retarde';
}
- let cause = '';
- if (!isTomorrow && delayMinutes > 0 && rtData.alerts.length > 0) {
- cause = rtData.alerts[idx % rtData.alerts.length].text;
+ let cause = (sx && sx.cause) || '';
+ if (!isTomorrow && delayMinutes > 0 && !cause) {
+ if (rtData.alerts.length > 0) {
+ cause = rtData.alerts[idx % rtData.alerts.length].text;
+ } else if (sxData.alerts.length > 0) {
+ cause = sxData.alerts[idx % sxData.alerts.length].text;
+ }
}
const formattedStops = (t.stops || []).map((st) => {
const stopUic = st.uic || '';
- const stopVoie = lookupPlatform(t.num, t.trip_id, stopUic, st.nom);
+ let stopVoie = lookupPlatform(t.num, t.trip_id, stopUic, st.nom, st.heure, destinationStation);
+ if (!stopVoie && (st.quai || st.quai_reel || st.voie || st.platform)) {
+ stopVoie = String(st.quai || st.quai_reel || st.voie || st.platform).replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
+ }
+ if (!stopVoie) {
+ stopVoie = isAutocar ? 'AUTOCAR' : resolveNominalTrack(st.nom, t.num);
+ }
return {
uic: stopUic,
@@ -580,13 +1044,10 @@ export async function getSncfTimetable({ gareName, mode = 'departs', quai = null
let finalTrains = null;
if (uic) {
finalTrains = await fetchGtfsStationTimetable(uic, mode);
- }
-
- // 2. Si non trouvé ou gare sans fichier, chercher avec le nom
- if (!finalTrains || finalTrains.length === 0) {
- const rawGare = gare ? gare.nom : gareName;
- const match = findGtfsGare(rawGare);
- if (match && match.uic && match.uic !== uic) {
+ } else {
+ // Si UIC non trouvé directement, chercher par nom
+ const match = findGtfsGare(gareName);
+ if (match && match.uic) {
finalTrains = await fetchGtfsStationTimetable(match.uic, mode);
}
}