RELEASE V5.6.0 : Voici les modifications apportées :

-> REFONTE TOTALE DE TOUS LES AFFICHEURS CLASSIQUES + QUAIS + OPTION TGV

FONCTIONNALITÉS EN COURS DE DÉVELOPPEMENT :

-> ADAPTABILITÉ EN FONCTION DES SITUATIONS
This commit is contained in:
mathis committed 2026-08-31 17:32:58 +02:00
1 parent 9c6ac798f1
commit d8f8ceaa9f
18 files changed
+2324 -533

No files matched your search

+40 -40
View File
@@ -135,121 +135,121 @@
}, },
{ {
"file": "logo-rer-a.svg", "file": "logo-rer-a.svg",
"path": "/img/type/logo-rer-a.svg", "path": "/logo-rer-a.svg",
"slug": "rer-a", "slug": "rer-a",
"name": "RER A" "name": "RER A"
}, },
{ {
"file": "logo-rer-b.svg", "file": "logo-rer-b.svg",
"path": "/img/type/logo-rer-b.svg", "path": "/logo-rer-b.svg",
"slug": "rer-b", "slug": "rer-b",
"name": "RER B" "name": "RER B"
}, },
{ {
"file": "logo-rer-c.svg", "file": "logo-rer-c.svg",
"path": "/img/type/logo-rer-c.svg", "path": "/logo-rer-c.svg",
"slug": "rer-c", "slug": "rer-c",
"name": "RER C" "name": "RER C"
}, },
{ {
"file": "logo-rer-d.svg", "file": "logo-rer-d.svg",
"path": "/img/type/logo-rer-d.svg", "path": "/logo-rer-d.svg",
"slug": "rer-d", "slug": "rer-d",
"name": "RER D" "name": "RER D"
}, },
{ {
"file": "logo-rer-e.svg", "file": "logo-rer-e.svg",
"path": "/img/type/logo-rer-e.svg", "path": "/logo-rer-e.svg",
"slug": "rer-e", "slug": "rer-e",
"name": "RER E" "name": "RER E"
}, },
{ {
"file": "logo-sbb.svg", "file": "logo-sbb.svg",
"path": "/img/type/logo-sbb.svg", "path": "/logo-sbb.svg",
"slug": "sbb", "slug": "sbb",
"name": "SBB" "name": "SBB"
}, },
{ {
"file": "logo-sncb.svg", "file": "logo-sncb.svg",
"path": "/img/type/logo-sncb.svg", "path": "/logo-sncb.svg",
"slug": "sncb", "slug": "sncb",
"name": "SNCB" "name": "SNCB"
}, },
{ {
"file": "logo-sncf.svg", "file": "logo-sncf.svg",
"path": "/img/type/logo-sncf.svg", "path": "/logo-sncf.svg",
"slug": "sncf", "slug": "sncf",
"name": "Sncf" "name": "Sncf"
}, },
{ {
"file": "sncf-voyageurs-logo.png", "file": "sncf-voyageurs-logo.png",
"path": "/img/type/sncf-voyageurs-logo.png", "path": "/sncf-voyageurs-logo.png",
"slug": "sncf-voyageurs-logo", "slug": "sncf-voyageurs-logo",
"name": "SNCF" "name": "SNCF"
}, },
{ {
"file": "logo-sncf1985.svg", "file": "logo-sncf1985.svg",
"path": "/img/type/logo-sncf1985.svg", "path": "/logo-sncf1985.svg",
"slug": "sncf1985", "slug": "sncf1985",
"name": "Sncf1985" "name": "Sncf1985"
}, },
{ {
"file": "logo-sncf1992.svg", "file": "logo-sncf1992.svg",
"path": "/img/type/logo-sncf1992.svg", "path": "/logo-sncf1992.svg",
"slug": "sncf1992", "slug": "sncf1992",
"name": "Sncf1992" "name": "Sncf1992"
}, },
{ {
"file": "logo-t-h.svg", "file": "logo-t-h.svg",
"path": "/img/type/logo-t-h.svg", "path": "/logo-t-h.svg",
"slug": "t-h", "slug": "t-h",
"name": "T H" "name": "T H"
}, },
{ {
"file": "logo-t-j.svg", "file": "logo-t-j.svg",
"path": "/img/type/logo-t-j.svg", "path": "/logo-t-j.svg",
"slug": "t-j", "slug": "t-j",
"name": "T J" "name": "T J"
}, },
{ {
"file": "logo-t-k.svg", "file": "logo-t-k.svg",
"path": "/img/type/logo-t-k.svg", "path": "/logo-t-k.svg",
"slug": "t-k", "slug": "t-k",
"name": "T K" "name": "T K"
}, },
{ {
"file": "logo-t-l.svg", "file": "logo-t-l.svg",
"path": "/img/type/logo-t-l.svg", "path": "/logo-t-l.svg",
"slug": "t-l", "slug": "t-l",
"name": "T L" "name": "T L"
}, },
{ {
"file": "logo-t-n.svg", "file": "logo-t-n.svg",
"path": "/img/type/logo-t-n.svg", "path": "/logo-t-n.svg",
"slug": "t-n", "slug": "t-n",
"name": "T N" "name": "T N"
}, },
{ {
"file": "logo-t-p.svg", "file": "logo-t-p.svg",
"path": "/img/type/logo-t-p.svg", "path": "/logo-t-p.svg",
"slug": "t-p", "slug": "t-p",
"name": "T P" "name": "T P"
}, },
{ {
"file": "logo-t-r.svg", "file": "logo-t-r.svg",
"path": "/img/type/logo-t-r.svg", "path": "/logo-t-r.svg",
"slug": "t-r", "slug": "t-r",
"name": "T R" "name": "T R"
}, },
{ {
"file": "logo-t-u.svg", "file": "logo-t-u.svg",
"path": "/img/type/logo-t-u.svg", "path": "/logo-t-u.svg",
"slug": "t-u", "slug": "t-u",
"name": "T U" "name": "T U"
}, },
{ {
"file": "logo-teoz.svg", "file": "logo-teoz.svg",
"path": "/img/type/logo-teoz.svg", "path": "/logo-teoz.svg",
"slug": "teoz", "slug": "teoz",
"name": "Teoz" "name": "Teoz"
}, },
@@ -261,121 +261,121 @@
}, },
{ {
"file": "logo-ter-rhone-alpes.svg", "file": "logo-ter-rhone-alpes.svg",
"path": "/img/type/logo-ter-rhone-alpes.svg", "path": "/logo-ter-rhone-alpes.svg",
"slug": "ter-rhone-alpes", "slug": "ter-rhone-alpes",
"name": "TER Rhone-Alpes" "name": "TER Rhone-Alpes"
}, },
{ {
"file": "logo-teralsace.svg", "file": "logo-teralsace.svg",
"path": "/img/type/logo-teralsace.svg", "path": "/logo-teralsace.svg",
"slug": "teralsace", "slug": "teralsace",
"name": "Ter Alsace" "name": "Ter Alsace"
}, },
{ {
"file": "logo-teraquitaine.svg", "file": "logo-teraquitaine.svg",
"path": "/img/type/logo-teraquitaine.svg", "path": "/logo-teraquitaine.svg",
"slug": "teraquitaine", "slug": "teraquitaine",
"name": "Ter Aquitaine" "name": "Ter Aquitaine"
}, },
{ {
"file": "logo-terbassenormandie.svg", "file": "logo-terbassenormandie.svg",
"path": "/img/type/logo-terbassenormandie.svg", "path": "/logo-terbassenormandie.svg",
"slug": "terbassenormandie", "slug": "terbassenormandie",
"name": "Ter Basse Normandie" "name": "Ter Basse Normandie"
}, },
{ {
"file": "logo-terbourgogne.svg", "file": "logo-terbourgogne.svg",
"path": "/img/type/logo-terbourgogne.svg", "path": "/logo-terbourgogne.svg",
"slug": "terbourgogne", "slug": "terbourgogne",
"name": "Ter Bourgogne" "name": "Ter Bourgogne"
}, },
{ {
"file": "logo-terbretagne.svg", "file": "logo-terbretagne.svg",
"path": "/img/type/logo-terbretagne.svg", "path": "/logo-terbretagne.svg",
"slug": "terbretagne", "slug": "terbretagne",
"name": "Ter Bretagne" "name": "Ter Bretagne"
}, },
{ {
"file": "logo-tercentre.svg", "file": "logo-tercentre.svg",
"path": "/img/type/logo-tercentre.svg", "path": "/logo-tercentre.svg",
"slug": "tercentre", "slug": "tercentre",
"name": "Ter Centre" "name": "Ter Centre"
}, },
{ {
"file": "logo-terlanguedocroussillon.svg", "file": "logo-terlanguedocroussillon.svg",
"path": "/img/type/logo-terlanguedocroussillon.svg", "path": "/logo-terlanguedocroussillon.svg",
"slug": "terlanguedocroussillon", "slug": "terlanguedocroussillon",
"name": "Ter Languedoc-Roussillon" "name": "Ter Languedoc-Roussillon"
}, },
{ {
"file": "logo-termidipyrenees.svg", "file": "logo-termidipyrenees.svg",
"path": "/img/type/logo-termidipyrenees.svg", "path": "/logo-termidipyrenees.svg",
"slug": "termidipyrenees", "slug": "termidipyrenees",
"name": "Ter Midi-Pyrenees" "name": "Ter Midi-Pyrenees"
}, },
{ {
"file": "logo-ternordpasdecalais.svg", "file": "logo-ternordpasdecalais.svg",
"path": "/img/type/logo-ternordpasdecalais.svg", "path": "/logo-ternordpasdecalais.svg",
"slug": "ternordpasdecalais", "slug": "ternordpasdecalais",
"name": "Ter Nord-pas-de-Calais" "name": "Ter Nord-pas-de-Calais"
}, },
{ {
"file": "logo-terpoitoucharentes.svg", "file": "logo-terpoitoucharentes.svg",
"path": "/img/type/logo-terpoitoucharentes.svg", "path": "/logo-terpoitoucharentes.svg",
"slug": "terpoitoucharentes", "slug": "terpoitoucharentes",
"name": "Ter Poitou Charentes" "name": "Ter Poitou Charentes"
}, },
{ {
"file": "logo-tgv.svg", "file": "logo-tgv.svg",
"path": "/img/type/logo-tgv.svg", "path": "/logo-tgv.svg",
"slug": "tgv", "slug": "tgv",
"name": "TGV" "name": "TGV"
}, },
{ {
"file": "logo-inoui.svg", "file": "logo-inoui.svg",
"path": "/img/type/logo-inoui.svg", "path": "/logo-inoui.svg",
"slug": "inoui", "slug": "inoui",
"name": "TGV InOui" "name": "TGV InOui"
}, },
{ {
"file": "logo-lyria.svg", "file": "logo-lyria.svg",
"path": "/img/type/logo-lyria.svg", "path": "/logo-lyria.svg",
"slug": "lyria", "slug": "lyria",
"name": "TGV Lyria" "name": "TGV Lyria"
}, },
{ {
"file": "logo-thalys.svg", "file": "logo-thalys.svg",
"path": "/img/type/logo-thalys.svg", "path": "/logo-thalys.svg",
"slug": "thalys", "slug": "thalys",
"name": "Thalys" "name": "Thalys"
}, },
{ {
"file": "logo-thello.svg", "file": "logo-thello.svg",
"path": "/img/type/logo-thello.svg", "path": "/logo-thello.svg",
"slug": "thello", "slug": "thello",
"name": "Thello" "name": "Thello"
}, },
{ {
"file": "track-car.svg", "file": "track-car.svg",
"path": "/img/type/track-car.svg", "path": "/track-car.svg",
"slug": "track-car", "slug": "track-car",
"name": "Car TER" "name": "Car TER"
}, },
{ {
"file": "logo-tramtrain.svg", "file": "logo-tramtrain.svg",
"path": "/img/type/logo-tramtrain.svg", "path": "/logo-tramtrain.svg",
"slug": "tramtrain", "slug": "tramtrain",
"name": "Tramtrain" "name": "Tramtrain"
}, },
{ {
"file": "logo-trenitalia.svg", "file": "logo-trenitalia.svg",
"path": "/img/type/logo-trenitalia.svg", "path": "/logo-trenitalia.svg",
"slug": "trenitalia", "slug": "trenitalia",
"name": "Trenitalia" "name": "Trenitalia"
}, },
{ {
"file": "logo-zou.svg", "file": "logo-zou.svg",
"path": "/img/type/logo-zou.svg", "path": "/logo-zou.svg",
"slug": "zou", "slug": "zou",
"name": "Zou" "name": "Zou"
} }
+63 -63
View File
@@ -3,379 +3,379 @@
"logos": [ "logos": [
{ {
"file": "logo-aleop-color.svg", "file": "logo-aleop-color.svg",
"path": "/type/quais/logo-aleop-color.svg", "path": "/logo-aleop-color.svg",
"slug": "aleop", "slug": "aleop",
"name": "Aleop" "name": "Aleop"
}, },
{ {
"file": "logo-auvergerhonealpes-color.svg", "file": "logo-auvergerhonealpes-color.svg",
"path": "/type/quais/logo-auvergerhonealpes-color.svg", "path": "/logo-auvergerhonealpes-color.svg",
"slug": "auvergerhonealpes", "slug": "auvergerhonealpes",
"name": "Auvergerhonealpes" "name": "Auvergerhonealpes"
}, },
{ {
"file": "logo-breizhgo-color.svg", "file": "logo-breizhgo-color.svg",
"path": "/type/quais/logo-breizhgo-color.svg", "path": "/logo-breizhgo-color.svg",
"slug": "breizhgo", "slug": "breizhgo",
"name": "Breizhgo" "name": "Breizhgo"
}, },
{ {
"file": "logo-cfl-color.svg", "file": "logo-cfl-color.svg",
"path": "/type/quais/logo-cfl-color.svg", "path": "/logo-cfl-color.svg",
"slug": "cfl", "slug": "cfl",
"name": "CFL" "name": "CFL"
}, },
{ {
"file": "logo-db-color.svg", "file": "logo-db-color.svg",
"path": "/type/quais/logo-db-color.svg", "path": "/logo-db-color.svg",
"slug": "db", "slug": "db",
"name": "DB" "name": "DB"
}, },
{ {
"file": "logo-eurostar-color.svg", "file": "logo-eurostar-color.svg",
"path": "/type/quais/logo-eurostar-color.svg", "path": "/logo-eurostar-color.svg",
"slug": "eurostar", "slug": "eurostar",
"name": "Eurostar" "name": "Eurostar"
}, },
{ {
"file": "logo-fluo-color.svg", "file": "logo-fluo-color.svg",
"path": "/type/quais/logo-fluo-color.svg", "path": "/logo-fluo-color.svg",
"slug": "fluo", "slug": "fluo",
"name": "Fluo" "name": "Fluo"
}, },
{ {
"file": "logo-frecciarossa-color.svg", "file": "logo-frecciarossa-color.svg",
"path": "/type/quais/logo-frecciarossa-color.svg", "path": "/logo-frecciarossa-color.svg",
"slug": "frecciarossa", "slug": "frecciarossa",
"name": "Frecciarossa" "name": "Frecciarossa"
}, },
{ {
"file": "logo-hautsdefrance-color.svg", "file": "logo-hautsdefrance-color.svg",
"path": "/type/quais/logo-hautsdefrance-color.svg", "path": "/logo-hautsdefrance-color.svg",
"slug": "hautsdefrance", "slug": "hautsdefrance",
"name": "Hautsdefrance" "name": "Hautsdefrance"
}, },
{ {
"file": "logo-ice-color.svg", "file": "logo-ice-color.svg",
"path": "/type/quais/logo-ice-color.svg", "path": "/logo-ice-color.svg",
"slug": "ice", "slug": "ice",
"name": "ICE" "name": "ICE"
}, },
{ {
"file": "logo-intercites-color.svg", "file": "logo-intercites-color.svg",
"path": "/type/quais/logo-intercites-color.svg", "path": "/logo-intercites-color.svg",
"slug": "intercites", "slug": "intercites",
"name": "Intercités" "name": "Intercités"
}, },
{ {
"file": "logo-ler-color.svg", "file": "logo-ler-color.svg",
"path": "/type/quais/logo-ler-color.svg", "path": "/logo-ler-color.svg",
"slug": "ler", "slug": "ler",
"name": "Ler" "name": "Ler"
}, },
{ {
"file": "logo-lio-color.svg", "file": "logo-lio-color.svg",
"path": "/type/quais/logo-lio-color.svg", "path": "/logo-lio-color.svg",
"slug": "lio", "slug": "lio",
"name": "Lio" "name": "Lio"
}, },
{ {
"file": "logo-lunea-color.svg", "file": "logo-lunea-color.svg",
"path": "/type/quais/logo-lunea-color.svg", "path": "/logo-lunea-color.svg",
"slug": "lunea", "slug": "lunea",
"name": "Lunea" "name": "Lunea"
}, },
{ {
"file": "logo-metrolor-color.svg", "file": "logo-metrolor-color.svg",
"path": "/type/quais/logo-metrolor-color.svg", "path": "/logo-metrolor-color.svg",
"slug": "metrolor", "slug": "metrolor",
"name": "Metrolor" "name": "Metrolor"
}, },
{ {
"file": "logo-mobigo-color.svg", "file": "logo-mobigo-color.svg",
"path": "/type/quais/logo-mobigo-color.svg", "path": "/logo-mobigo-color.svg",
"slug": "mobigo", "slug": "mobigo",
"name": "Mobigo" "name": "Mobigo"
}, },
{ {
"file": "logo-nomad-color.svg", "file": "logo-nomad-color.svg",
"path": "/type/quais/logo-nomad-color.svg", "path": "/logo-nomad-color.svg",
"slug": "nomad", "slug": "nomad",
"name": "Nomad" "name": "Nomad"
}, },
{ {
"file": "logo-occitanie-color.svg", "file": "logo-occitanie-color.svg",
"path": "/type/quais/logo-occitanie-color.svg", "path": "/logo-occitanie-color.svg",
"slug": "occitanie", "slug": "occitanie",
"name": "Occitanie" "name": "Occitanie"
}, },
{ {
"file": "logo-ouigo-color.svg", "file": "logo-ouigo-color.svg",
"path": "/type/quais/logo-ouigo-color.svg", "path": "/logo-ouigo-color.svg",
"slug": "ouigo", "slug": "ouigo",
"name": "OUIGO" "name": "OUIGO"
}, },
{ {
"file": "logo-ouigo-classique-color.svg", "file": "logo-ouigo-classique-color.svg",
"path": "/type/quais/logo-ouigo-classique-color.svg", "path": "/logo-ouigo-classique-color.svg",
"slug": "ouigo-classique", "slug": "ouigo-classique",
"name": "OUIGO Trains Classiques" "name": "OUIGO Trains Classiques"
}, },
{ {
"file": "logo-remi-color.svg", "file": "logo-remi-color.svg",
"path": "/type/quais/logo-remi-color.svg", "path": "/logo-remi-color.svg",
"slug": "remi", "slug": "remi",
"name": "Remi" "name": "Remi"
}, },
{ {
"file": "logo-renfeave-color.svg", "file": "logo-renfeave-color.svg",
"path": "/type/quais/logo-renfeave-color.svg", "path": "/logo-renfeave-color.svg",
"slug": "renfeave", "slug": "renfeave",
"name": "Renfe AVE" "name": "Renfe AVE"
}, },
{ {
"file": "logo-rer-a-color.svg", "file": "logo-rer-a-color.svg",
"path": "/type/quais/logo-rer-a-color.svg", "path": "/logo-rer-a-color.svg",
"slug": "rer-a", "slug": "rer-a",
"name": "RER A" "name": "RER A"
}, },
{ {
"file": "logo-rer-b-color.svg", "file": "logo-rer-b-color.svg",
"path": "/type/quais/logo-rer-b-color.svg", "path": "/logo-rer-b-color.svg",
"slug": "rer-b", "slug": "rer-b",
"name": "RER B" "name": "RER B"
}, },
{ {
"file": "logo-rer-c-color.svg", "file": "logo-rer-c-color.svg",
"path": "/type/quais/logo-rer-c-color.svg", "path": "/logo-rer-c-color.svg",
"slug": "rer-c", "slug": "rer-c",
"name": "RER C" "name": "RER C"
}, },
{ {
"file": "logo-rer-d-color.svg", "file": "logo-rer-d-color.svg",
"path": "/type/quais/logo-rer-d-color.svg", "path": "/logo-rer-d-color.svg",
"slug": "rer-d", "slug": "rer-d",
"name": "RER D" "name": "RER D"
}, },
{ {
"file": "logo-rer-e-color.svg", "file": "logo-rer-e-color.svg",
"path": "/type/quais/logo-rer-e-color.svg", "path": "/logo-rer-e-color.svg",
"slug": "rer-e", "slug": "rer-e",
"name": "RER E" "name": "RER E"
}, },
{ {
"file": "logo-sbb-color.svg", "file": "logo-sbb-color.svg",
"path": "/type/quais/logo-sbb-color.svg", "path": "/logo-sbb-color.svg",
"slug": "sbb", "slug": "sbb",
"name": "SBB" "name": "SBB"
}, },
{ {
"file": "logo-sncb-color.svg", "file": "logo-sncb-color.svg",
"path": "/type/quais/logo-sncb-color.svg", "path": "/logo-sncb-color.svg",
"slug": "sncb", "slug": "sncb",
"name": "SNCB" "name": "SNCB"
}, },
{ {
"file": "logo-sncf-color.svg", "file": "logo-sncf-color.svg",
"path": "/type/quais/logo-sncf-color.svg", "path": "/logo-sncf-color.svg",
"slug": "sncf", "slug": "sncf",
"name": "Sncf" "name": "Sncf"
}, },
{ {
"file": "sncf-voyageurs-logo-color.png", "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", "slug": "sncf-voyageurs-logo",
"name": "SNCF" "name": "SNCF"
}, },
{ {
"file": "logo-sncf1985.svg", "file": "logo-sncf1985.svg",
"path": "/type/quais/img/type/logo-sncf1985.svg", "path": "/img/type/logo-sncf1985.svg",
"slug": "sncf1985", "slug": "sncf1985",
"name": "Sncf1985" "name": "Sncf1985"
}, },
{ {
"file": "logo-sncf1992.svg", "file": "logo-sncf1992.svg",
"path": "/type/quais/img/type/logo-sncf1992.svg", "path": "/img/type/logo-sncf1992.svg",
"slug": "sncf1992", "slug": "sncf1992",
"name": "Sncf1992" "name": "Sncf1992"
}, },
{ {
"file": "logo-t-h.svg", "file": "logo-t-h.svg",
"path": "/type/quais/img/type/logo-t-h.svg", "path": "/img/type/logo-t-h.svg",
"slug": "t-h", "slug": "t-h",
"name": "T H" "name": "T H"
}, },
{ {
"file": "logo-t-j.svg", "file": "logo-t-j.svg",
"path": "/type/quais/img/type/logo-t-j.svg", "path": "/img/type/logo-t-j.svg",
"slug": "t-j", "slug": "t-j",
"name": "T J" "name": "T J"
}, },
{ {
"file": "logo-t-k.svg", "file": "logo-t-k.svg",
"path": "/type/quais/img/type/logo-t-k.svg", "path": "/img/type/logo-t-k.svg",
"slug": "t-k", "slug": "t-k",
"name": "T K" "name": "T K"
}, },
{ {
"file": "logo-t-l.svg", "file": "logo-t-l.svg",
"path": "/type/quais/img/type/logo-t-l.svg", "path": "/img/type/logo-t-l.svg",
"slug": "t-l", "slug": "t-l",
"name": "T L" "name": "T L"
}, },
{ {
"file": "logo-t-n.svg", "file": "logo-t-n.svg",
"path": "/type/quais/img/type/logo-t-n.svg", "path": "/img/type/logo-t-n.svg",
"slug": "t-n", "slug": "t-n",
"name": "T N" "name": "T N"
}, },
{ {
"file": "logo-t-p.svg", "file": "logo-t-p.svg",
"path": "/type/quais/img/type/logo-t-p.svg", "path": "/img/type/logo-t-p.svg",
"slug": "t-p", "slug": "t-p",
"name": "T P" "name": "T P"
}, },
{ {
"file": "logo-t-r.svg", "file": "logo-t-r.svg",
"path": "/type/quais/img/type/logo-t-r.svg", "path": "/img/type/logo-t-r.svg",
"slug": "t-r", "slug": "t-r",
"name": "T R" "name": "T R"
}, },
{ {
"file": "logo-t-u.svg", "file": "logo-t-u.svg",
"path": "/type/quais/img/type/logo-t-u.svg", "path": "/img/type/logo-t-u.svg",
"slug": "t-u", "slug": "t-u",
"name": "T U" "name": "T U"
}, },
{ {
"file": "logo-teoz.svg", "file": "logo-teoz.svg",
"path": "/type/quais/img/type/logo-teoz.svg", "path": "/img/type/logo-teoz.svg",
"slug": "teoz", "slug": "teoz",
"name": "Teoz" "name": "Teoz"
}, },
{ {
"file": "sncf-voyageurs-logo.png", "file": "sncf-voyageurs-logo.png",
"path": "/type/quais/sncf-voyageurs-logo.png", "path": "/sncf-voyageurs-logo.png",
"slug": "ter", "slug": "ter",
"name": "TER" "name": "TER"
}, },
{ {
"file": "logo-ter-rhone-alpes.svg", "file": "logo-ter-rhone-alpes.svg",
"path": "/type/quais/logo-ter-rhone-alpes.svg", "path": "/logo-ter-rhone-alpes.svg",
"slug": "ter-rhone-alpes", "slug": "ter-rhone-alpes",
"name": "TER Rhone Alpes" "name": "TER Rhone Alpes"
}, },
{ {
"file": "logo-teralsace.svg", "file": "logo-teralsace.svg",
"path": "/type/quais/logo-teralsace.svg", "path": "/logo-teralsace.svg",
"slug": "teralsace", "slug": "teralsace",
"name": "Teralsace" "name": "Teralsace"
}, },
{ {
"file": "logo-teraquitaine.svg", "file": "logo-teraquitaine.svg",
"path": "/type/quais/logo-teraquitaine.svg", "path": "/logo-teraquitaine.svg",
"slug": "teraquitaine", "slug": "teraquitaine",
"name": "Teraquitaine" "name": "Teraquitaine"
}, },
{ {
"file": "logo-terbassenormandie.svg", "file": "logo-terbassenormandie.svg",
"path": "/type/quais/logo-terbassenormandie.svg", "path": "/logo-terbassenormandie.svg",
"slug": "terbassenormandie", "slug": "terbassenormandie",
"name": "Terbassenormandie" "name": "Terbassenormandie"
}, },
{ {
"file": "logo-terbourgogne.svg", "file": "logo-terbourgogne.svg",
"path": "/type/quais/logo-terbourgogne.svg", "path": "/logo-terbourgogne.svg",
"slug": "terbourgogne", "slug": "terbourgogne",
"name": "Terbourgogne" "name": "Terbourgogne"
}, },
{ {
"file": "logo-terbretagne.svg", "file": "logo-terbretagne.svg",
"path": "/type/quais/logo-terbretagne.svg", "path": "/logo-terbretagne.svg",
"slug": "terbretagne", "slug": "terbretagne",
"name": "Terbretagne" "name": "Terbretagne"
}, },
{ {
"file": "logo-tercentre.svg", "file": "logo-tercentre.svg",
"path": "/type/quais/logo-tercentre.svg", "path": "/logo-tercentre.svg",
"slug": "tercentre", "slug": "tercentre",
"name": "Tercentre" "name": "Tercentre"
}, },
{ {
"file": "logo-terlanguedocroussillon.svg", "file": "logo-terlanguedocroussillon.svg",
"path": "/type/quais/logo-terlanguedocroussillon.svg", "path": "/logo-terlanguedocroussillon.svg",
"slug": "terlanguedocroussillon", "slug": "terlanguedocroussillon",
"name": "Terlanguedocroussillon" "name": "Terlanguedocroussillon"
}, },
{ {
"file": "logo-termidipyrenees.svg", "file": "logo-termidipyrenees.svg",
"path": "/type/quais/logo-termidipyrenees.svg", "path": "/logo-termidipyrenees.svg",
"slug": "termidipyrenees", "slug": "termidipyrenees",
"name": "Termidipyrenees" "name": "Termidipyrenees"
}, },
{ {
"file": "logo-ternordpasdecalais.svg", "file": "logo-ternordpasdecalais.svg",
"path": "/type/quais/logo-ternordpasdecalais.svg", "path": "/logo-ternordpasdecalais.svg",
"slug": "ternordpasdecalais", "slug": "ternordpasdecalais",
"name": "Ternordpasdecalais" "name": "Ternordpasdecalais"
}, },
{ {
"file": "logo-terpoitoucharentes.svg", "file": "logo-terpoitoucharentes.svg",
"path": "/type/quais/logo-terpoitoucharentes.svg", "path": "/logo-terpoitoucharentes.svg",
"slug": "terpoitoucharentes", "slug": "terpoitoucharentes",
"name": "Terpoitoucharentes" "name": "Terpoitoucharentes"
}, },
{ {
"file": "logo-tgv.svg", "file": "logo-tgv.svg",
"path": "/type/quais/logo-tgv.svg", "path": "/logo-tgv.svg",
"slug": "tgv", "slug": "tgv",
"name": "TGV" "name": "TGV"
}, },
{ {
"file": "logo-inoui.svg", "file": "logo-inoui.svg",
"path": "/type/quais/logo-inoui.svg", "path": "/logo-inoui.svg",
"slug": "inoui", "slug": "inoui",
"name": "TGV InOui" "name": "TGV InOui"
}, },
{ {
"file": "logo-lyria.svg", "file": "logo-lyria.svg",
"path": "/type/quais/logo-lyria.svg", "path": "/logo-lyria.svg",
"slug": "lyria", "slug": "lyria",
"name": "TGV Lyria" "name": "TGV Lyria"
}, },
{ {
"file": "logo-thalys.svg", "file": "logo-thalys.svg",
"path": "/type/quais/logo-thalys.svg", "path": "/logo-thalys.svg",
"slug": "thalys", "slug": "thalys",
"name": "Thalys" "name": "Thalys"
}, },
{ {
"file": "logo-thello.svg", "file": "logo-thello.svg",
"path": "/type/quais/logo-thello.svg", "path": "/logo-thello.svg",
"slug": "thello", "slug": "thello",
"name": "Thello" "name": "Thello"
}, },
{ {
"file": "track-car.svg", "file": "track-car.svg",
"path": "/type/quais/track-car.svg", "path": "/track-car.svg",
"slug": "track-car", "slug": "track-car",
"name": "Track Car" "name": "Track Car"
}, },
{ {
"file": "logo-tramtrain.svg", "file": "logo-tramtrain.svg",
"path": "/type/quais/logo-tramtrain.svg", "path": "/logo-tramtrain.svg",
"slug": "tramtrain", "slug": "tramtrain",
"name": "Tramtrain" "name": "Tramtrain"
}, },
{ {
"file": "logo-trenitalia.svg", "file": "logo-trenitalia.svg",
"path": "/type/quais/logo-trenitalia.svg", "path": "/logo-trenitalia.svg",
"slug": "trenitalia", "slug": "trenitalia",
"name": "Trenitalia" "name": "Trenitalia"
}, },
{ {
"file": "logo-zou.svg", "file": "logo-zou.svg",
"path": "/type/quais/logo-zou.svg", "path": "/logo-zou.svg",
"slug": "zou", "slug": "zou",
"name": "Zou" "name": "Zou"
} }
@@ -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;
}
@@ -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 (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4L12 14L20 4H4Z" stroke={color} strokeWidth="2.2" strokeLinejoin="round" fill={color} />
<path d="M12 14V21M8 21H16" stroke={color} strokeWidth="2.2" strokeLinecap="round" />
</svg>
);
}
/**
* 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 (
<div className="aff-tgv-comp__clock">
<span className="aff-tgv-comp__clock-hm">{hh}:{mm}</span>
<span className="aff-tgv-comp__clock-s">{ss}</span>
</div>
);
}
/**
* 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 (
<div className="aff-tgv-comp__rame-block">
{destination && (
<div className="aff-tgv-comp__dest-title">
{destination}
</div>
)}
{/* Numéros des voitures */}
<div className="aff-tgv-comp__coaches-row">
{/* Espace motrice avant */}
<div className="aff-tgv-comp__coach-num-motrice" />
{coaches.map((c, idx) => (
<div key={idx} className="aff-tgv-comp__coach-num-box">
{c === 'bar' ? (
<BarGlassIcon size={20} color="#ffffff" />
) : (
<span>{c}</span>
)}
</div>
))}
{/* Espace motrice arrière */}
<div className="aff-tgv-comp__coach-num-motrice" />
</div>
{/* Silhouette du train TGV */}
<div className="aff-tgv-comp__train-graphic">
{/* Motrice avant arrondie */}
<div className="aff-tgv-comp__loco-front" />
{/* Voitures 1 à 8 */}
<div className="aff-tgv-comp__coaches-body">
{coaches.map((c, idx) => (
<div
key={idx}
className={`aff-tgv-comp__coach-segment ${c === 'bar' ? 'aff-tgv-comp__coach-segment--bar' : ''}`}
/>
))}
</div>
{/* Motrice arrière arrondie */}
<div className="aff-tgv-comp__loco-rear" />
</div>
</div>
);
}
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 (
<div className="aff-tgv-comp__container">
{/* LIGNE DU HAUT : BOX VOIE + VISUALISATION DU TRAIN */}
<div className="aff-tgv-comp__top-row">
{/* Indicateur Voie à gauche */}
<div className="aff-tgv-comp__track-box">
<div className="aff-tgv-comp__track-pill">Voie</div>
<div className="aff-tgv-comp__track-letter">{cleanTrack ? cleanTrack.toUpperCase() : '-'}</div>
<div className="aff-tgv-comp__track-underline" />
</div>
{/* Visualisation Composition des Rames */}
<div className="aff-tgv-comp__rames-container">
<TgvRameShape
destination={destination}
coaches={rame1Coaches}
/>
<TgvRameShape
destination={isMultiDest ? destination2 : destination}
coaches={rame2Coaches}
/>
</div>
</div>
{/* LIGNE POINTILLÉE AVEC REPÈRES DE QUAI (Conforme Image 2) */}
<div className="aff-tgv-comp__reperes-line-wrapper">
<div className="aff-tgv-comp__dotted-line" />
{reperes && reperes.length > 0 && (
<div className="aff-tgv-comp__reperes-row">
{reperes.map((rep, idx) => (
<div key={idx} className="aff-tgv-comp__repere-box">
{rep}
</div>
))}
</div>
)}
</div>
{/* LIGNE DU BAS : DÉFILEMENT MESSAGE + HORLOGE */}
<div className="aff-tgv-comp__bottom-row">
<div className="aff-tgv-comp__marquee-wrapper">
<div className="aff-tgv-comp__marquee-text">
{defaultMarquee}
</div>
</div>
<div className="aff-tgv-comp__clock-wrapper">
<TgvDigitalClock />
</div>
</div>
</div>
);
}
+34 -25
View File
@@ -6,7 +6,8 @@ import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService'; 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 AFLSecurityPage from './components/AFLSecurityPage';
import AFLCorrespondancesPage from './components/AFLCorrespondancesPage'; import AFLCorrespondancesPage from './components/AFLCorrespondancesPage';
import AFLInformationsPage from './components/AFLInformationsPage'; import AFLInformationsPage from './components/AFLInformationsPage';
@@ -219,12 +220,8 @@ export default function AFLArrivees(){
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`; const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle); const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
if (!gerr && gdata && gdata.length) { if (!gerr && gdata && gdata.length) {
const exactMatch = gdata.find(g => { const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim(); const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
return n1 === n2;
});
const chosen = exactMatch || gdata[0];
fetchedType = chosen ? chosen.type : null; fetchedType = chosen ? chosen.type : null;
fetchedId = chosen ? chosen.id : null; fetchedId = chosen ? chosen.id : null;
} }
@@ -662,17 +659,18 @@ export default function AFLArrivees(){
const stopForStation = (stops || []).find(s => { const stopForStation = (stops || []).find(s => {
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null; 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 nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
const ns = normalizeName(nm || ''); return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
const target = normalizeName(targetStationName || '');
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
}) || null; }) || null;
horaire._status = displayStatus; horaire._status = displayStatus;
horaire._displayTime = displayTime; 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 || ''; 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 || '';
horaire._displayPlatform = String(platformVal).trim(); 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; 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 // Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu
useEffect(() => { useEffect(() => {
@@ -699,11 +697,13 @@ export default function AFLArrivees(){
} else { } else {
const formatted = (sncfData || []).map(t => { const formatted = (sncfData || []).map(t => {
const timeStr = t.heure_arrivee || t.heure_depart || t.time || ''; 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 { return {
...t, ...t,
_displayTime: formatTimeNoSeconds(timeStr), _displayTime: formatTimeNoSeconds(timeStr),
_displayPlatform: t.quai || t.voie || '', _displayPlatform: plat,
_arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr), _arrHM: arrHM,
_status: t._status || { _status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"), label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '', detail: '',
@@ -784,25 +784,24 @@ export default function AFLArrivees(){
// comme une gare d'arrivée (soit le terminus, soit un arrêt intermédiaire) // comme une gare d'arrivée (soit le terminus, soit un arrêt intermédiaire)
let displayHoraires = parsedHoraires.slice(); let displayHoraires = parsedHoraires.slice();
if (gareDisplay) { if (gareDisplay) {
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
displayHoraires = displayHoraires.filter(h => { 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) // 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()); const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
if (departName === normalized) return false; if (matchStationNameStrict(departName, gareDisplay)) return false;
// Inclure si gare d'arrivée finale correspond // Inclure si gare d'arrivée finale correspond
const arriveeName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString()); const arriveeName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
if (arriveeName === normalized) return true; if (matchStationNameStrict(arriveeName, gareDisplay)) return true;
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est l'origine (isFirst ou index==0) // Chercher dans les arrêts desservis ; ignorer si l'arrêt est l'origine (isFirst ou index==0)
const stopsList = getStops(h); const stopsList = getStops(h);
if (Array.isArray(stopsList) && stopsList.length) { if (Array.isArray(stopsList) && stopsList.length) {
for (const s of stopsList) { 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; if (!stopName) continue;
const isOrigin = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && Number(s.index) === 0)); 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 (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; return false;
@@ -868,7 +867,7 @@ export default function AFLArrivees(){
const stopsListForH = getStops(h); const stopsListForH = getStops(h);
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) { if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null; 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; const stop = byId || byName;
if (stop && (stop.heure_arrivee || stop.time || stop.heure)) raw = stop.heure_arrivee || stop.time || stop.heure; 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(); ).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 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 || ''); let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure || '');
const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue'; const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue';
+40 -27
View File
@@ -6,6 +6,7 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import Logo from '../../../components/Logo'; import Logo from '../../../components/Logo';
import { getSourceFromPath } from '../../../services/railwayService'; import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi'; import { getSncfTimetable } from '../../../services/sncfApi';
import { matchStationNameStrict } from '../../../services/gtfsStations';
import GareVideoprotection from '../../../components/afficheurs/GareVideoprotection'; import GareVideoprotection from '../../../components/afficheurs/GareVideoprotection';
import AttentifsEnsemble from '../../../components/afficheurs/AttentifsEnsemble'; import AttentifsEnsemble from '../../../components/afficheurs/AttentifsEnsemble';
import AFLInformationsPage from './components/AFLInformationsPage'; import AFLInformationsPage from './components/AFLInformationsPage';
@@ -373,22 +374,36 @@ const AFLArriveesQuais = () => {
quai: quaiParam quai: quaiParam
}); });
if (sncfData && sncfData.length > 0) { if (sncfData && sncfData.length > 0) {
const train = sncfData[0]; let matching = sncfData;
const enriched = { if (quaiParam) {
...train, const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
_rawTime: train.heure_arrivee || train.heure_depart, matching = sncfData.filter(t => cleanP(t._displayPlatform || t.quai || t.voie) === cleanP(quaiParam));
_displayTime: formatTimeNoSeconds(train.heure_arrivee || train.heure_depart), }
_status: { const train = matching[0];
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"), if (train) {
detail: '', const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || '';
delayMinutes: train.retard_minutes || null const enriched = {
}, ...train,
_nextStationStatus: { quai: trainPlatform,
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure") quai_reel: trainPlatform,
}, voie: trainPlatform,
_isToday: true _displayPlatform: trainPlatform,
}; _rawTime: train.heure_arrivee || train.heure_depart,
setNextArrival(enriched); _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 { } else {
setNextArrival(null); setNextArrival(null);
} }
@@ -591,25 +606,24 @@ const AFLArriveesQuais = () => {
// Filter by station: keep arrivals to this station or stops at this station (excluding origin) // Filter by station: keep arrivals to this station or stops at this station (excluding origin)
let stationFiltered = enriched; let stationFiltered = enriched;
if (gareDisplay) { if (gareDisplay) {
const normalized = normalizeName(gareDisplay);
stationFiltered = enriched.filter(h => { stationFiltered = enriched.filter(h => {
// Exclude if target station is the departure origin // Exclude if target station is the departure origin
const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString()); const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
if (departName === normalized) return false; if (matchStationNameStrict(departName, gareDisplay)) return false;
// Include if target station is the terminus // Include if target station is the terminus
const arriveeName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString()); const arriveeName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
if (arriveeName === normalized) return true; if (matchStationNameStrict(arriveeName, gareDisplay)) return true;
// Check intermediate stops // Check intermediate stops
const stopsList = getStops(h); const stopsList = getStops(h);
if (Array.isArray(stopsList) && stopsList.length) { if (Array.isArray(stopsList) && stopsList.length) {
for (const s of stopsList) { 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; if (!stopName) continue;
const isOrigin = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && Number(s.index) === 0)); const isOrigin = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && Number(s.index) === 0));
if (isOrigin) continue; if (isOrigin) continue;
if (stopName === normalized) return true; if (matchStationNameStrict(stopName, gareDisplay)) return true;
} }
} }
return false; return false;
@@ -676,7 +690,7 @@ const AFLArriveesQuais = () => {
if (gareDisplay && Array.isArray(stopsList)) { if (gareDisplay && Array.isArray(stopsList)) {
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)); 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; if (!quaiParam) return true;
const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim(); const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
return cleanP(platform) === cleanP(quaiParam); return cleanP(platform) === cleanP(quaiParam);
@@ -862,10 +876,9 @@ const AFLArriveesQuais = () => {
let journeyStops = []; let journeyStops = [];
if (Array.isArray(stopsList) && stopsList.length > 0) { if (Array.isArray(stopsList) && stopsList.length > 0) {
const normCurrent = normalizeName(currentStationName);
const endIdx = stopsList.findIndex(s => { const endIdx = stopsList.findIndex(s => {
const sName = normalizeName((s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString()); const sName = (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)); return sName && matchStationNameStrict(sName, currentStationName);
}); });
const rawStops = endIdx !== -1 ? stopsList.slice(0, endIdx + 1) : stopsList.slice(); const rawStops = endIdx !== -1 ? stopsList.slice(0, endIdx + 1) : stopsList.slice();
+45 -34
View File
@@ -6,7 +6,8 @@ import MarqueeText from '../../../components/MarqueeText';
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../services/railwayService'; 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 AFLSecurityPage from './components/AFLSecurityPage';
import AFLCorrespondancesPage from './components/AFLCorrespondancesPage'; import AFLCorrespondancesPage from './components/AFLCorrespondancesPage';
import AFLInformationsPage from './components/AFLInformationsPage'; import AFLInformationsPage from './components/AFLInformationsPage';
@@ -227,12 +228,8 @@ export default function AFLDeparts(){
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`; const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle); const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
if (!gerr && gdata && gdata.length) { if (!gerr && gdata && gdata.length) {
const exactMatch = gdata.find(g => { const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim(); const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
return n1 === n2;
});
const chosen = exactMatch || gdata[0];
fetchedType = chosen ? chosen.type : null; fetchedType = chosen ? chosen.type : null;
fetchedId = chosen ? chosen.id : null; fetchedId = chosen ? chosen.id : null;
} }
@@ -670,17 +667,18 @@ export default function AFLDeparts(){
const stopForStation = (stops || []).find(s => { const stopForStation = (stops || []).find(s => {
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null; 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 nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
const ns = normalizeName(nm || ''); return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
const target = normalizeName(targetStationName || '');
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
}) || null; }) || null;
horaire._status = displayStatus; horaire._status = displayStatus;
horaire._displayTime = displayTime; 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 || ''; 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 || '';
horaire._displayPlatform = String(platformVal).trim(); 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; return horaire;
}); });
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]); }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
useEffect(() => { useEffect(() => {
const id = setInterval(() => setShowStatus(s => !s), 3000); const id = setInterval(() => setShowStatus(s => !s), 3000);
@@ -710,18 +708,22 @@ export default function AFLDeparts(){
if (sncfErr) { if (sncfErr) {
setError(sncfErr); setError(sncfErr);
} else { } else {
const formatted = (sncfData || []).map(t => ({ const formatted = (sncfData || []).map(t => {
...t, const depHM = (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart);
_displayTime: formatTimeNoSeconds(t.heure_depart), const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
_displayPlatform: t.quai || t.voie || '', return {
_depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart), ...t,
_status: t._status || { _displayTime: formatTimeNoSeconds(t.heure_depart),
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"), _displayPlatform: plat,
detail: '', _depHM: depHM,
delayMinutes: t.retard_minutes || null, _status: t._status || {
causeDetails: t.cause_retard || null 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); setHoraires(formatted);
setAllHoraires(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) // Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
let displayHoraires = parsedHoraires.slice(); let displayHoraires = parsedHoraires.slice();
if (gareDisplay) { if (gareDisplay) {
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
displayHoraires = displayHoraires.filter(h => { displayHoraires = displayHoraires.filter(h => {
// Exclure si la gare ciblée est la gare d'arrivée finale // Exclure si la gare ciblée est la gare d'arrivée finale
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString()); const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
if (destName === normalized) return false; if (matchStationNameStrict(destName, gareDisplay)) return false;
// Inclure si gare de départ correspond // Inclure si gare de départ correspond
const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString()); const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString();
if (departName === normalized) return true; if (matchStationNameStrict(departName, gareDisplay)) return true;
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max) // Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max)
const stopsList = getStops(h); const stopsList = getStops(h);
@@ -816,11 +817,11 @@ export default function AFLDeparts(){
} }
} }
for (const s of stopsList) { 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; if (!stopName) continue;
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx)); const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
if (isTerminus) continue; if (isTerminus) continue;
if (stopName === normalized) return true; if (matchStationNameStrict(stopName, gareDisplay)) return true;
} }
} }
return false; return false;
@@ -893,7 +894,7 @@ export default function AFLDeparts(){
const stopsListForH = getStops(h); const stopsListForH = getStops(h);
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) { if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null; 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; const stop = byId || byName;
if (stop && (stop.heure_depart || stop.time || stop.heure)) raw = stop.heure_depart || stop.time || stop.heure; 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(); ).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 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 || ''); let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
const destination = horaire.gare_arrivee_nom || ''; const destination = horaire.gare_arrivee_nom || '';
+41 -28
View File
@@ -6,6 +6,7 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import Logo from '../../../components/Logo'; import Logo from '../../../components/Logo';
import { getSourceFromPath } from '../../../services/railwayService'; import { getSourceFromPath } from '../../../services/railwayService';
import { getSncfTimetable } from '../../../services/sncfApi'; import { getSncfTimetable } from '../../../services/sncfApi';
import { matchStationNameStrict } from '../../../services/gtfsStations';
import GareVideoprotection from '../../../components/afficheurs/GareVideoprotection'; import GareVideoprotection from '../../../components/afficheurs/GareVideoprotection';
import AttentifsEnsemble from '../../../components/afficheurs/AttentifsEnsemble'; import AttentifsEnsemble from '../../../components/afficheurs/AttentifsEnsemble';
import DepartEnCoursAFL from '../../../components/afficheurs/DepartEnCoursAFL'; import DepartEnCoursAFL from '../../../components/afficheurs/DepartEnCoursAFL';
@@ -445,22 +446,36 @@ const AFLDepartsQuais = () => {
quai: quaiParam quai: quaiParam
}); });
if (sncfData && sncfData.length > 0) { if (sncfData && sncfData.length > 0) {
const train = sncfData[0]; let matching = sncfData;
const enriched = { if (quaiParam) {
...train, const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
_rawTime: train.heure_depart, matching = sncfData.filter(t => cleanP(t._displayPlatform || t.quai || t.voie) === cleanP(quaiParam));
_displayTime: formatTimeNoSeconds(train.heure_depart), }
_status: { const train = matching[0];
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"), if (train) {
detail: '', const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || '';
delayMinutes: train.retard_minutes || null const enriched = {
}, ...train,
_nextStationStatus: { quai: trainPlatform,
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure") quai_reel: trainPlatform,
}, voie: trainPlatform,
_isToday: true _displayPlatform: trainPlatform,
}; _rawTime: train.heure_depart,
setNextDeparture(enriched); _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 { } else {
setNextDeparture(null); setNextDeparture(null);
} }
@@ -663,13 +678,12 @@ const AFLDepartsQuais = () => {
// Filter by station: keep departures from this station or stops at this station (excluding terminus) // Filter by station: keep departures from this station or stops at this station (excluding terminus)
let stationFiltered = enriched; let stationFiltered = enriched;
if (gareDisplay) { if (gareDisplay) {
const normalized = normalizeName(gareDisplay);
stationFiltered = enriched.filter(h => { stationFiltered = enriched.filter(h => {
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString()); const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
if (destName === normalized) return false; if (matchStationNameStrict(destName, gareDisplay)) return false;
const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString()); const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
if (departName === normalized) return true; if (matchStationNameStrict(departName, gareDisplay)) return true;
const stopsList = getStops(h); const stopsList = getStops(h);
if (Array.isArray(stopsList) && stopsList.length) { if (Array.isArray(stopsList) && stopsList.length) {
@@ -681,11 +695,11 @@ const AFLDepartsQuais = () => {
} }
} }
for (const s of stopsList) { 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; if (!stopName) continue;
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx)); const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
if (isTerminus) continue; if (isTerminus) continue;
if (stopName === normalized) return true; if (matchStationNameStrict(stopName, gareDisplay)) return true;
} }
} }
return false; return false;
@@ -751,7 +765,7 @@ const AFLDepartsQuais = () => {
if (gareDisplay && Array.isArray(stopsList)) { if (gareDisplay && Array.isArray(stopsList)) {
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)); 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; if (!quaiParam) return true;
const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim(); const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
return cleanP(platform) === cleanP(quaiParam); return cleanP(platform) === cleanP(quaiParam);
@@ -964,10 +978,9 @@ const AFLDepartsQuais = () => {
let downstreamStops = []; let downstreamStops = [];
if (Array.isArray(stopsList) && stopsList.length > 0) { if (Array.isArray(stopsList) && stopsList.length > 0) {
const normCurrent = normalizeName(currentStationName);
const startIdx = stopsList.findIndex(s => { const startIdx = stopsList.findIndex(s => {
const sName = normalizeName((s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString()); const sName = (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)); return sName && matchStationNameStrict(sName, currentStationName);
}); });
const rawDownstream = startIdx !== -1 ? stopsList.slice(startIdx + 1) : stopsList; const rawDownstream = startIdx !== -1 ? stopsList.slice(startIdx + 1) : stopsList;
@@ -976,7 +989,7 @@ const AFLDepartsQuais = () => {
rawDownstream.forEach(s => { 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 name = (s.gare_nom || s.nom || s.name || s.station_name || s.stop_name || (typeof s === 'string' ? s : '')).toString().trim();
const norm = normalizeName(name); const norm = normalizeName(name);
if (name && norm !== normCurrent && !seen.has(norm)) { if (name && !matchStationNameStrict(name, currentStationName) && !seen.has(norm)) {
seen.add(norm); seen.add(norm);
downstreamStops.push(name); downstreamStops.push(name);
} }
@@ -248,11 +248,25 @@
.aff-classiques-arrivees__status-delayed { .aff-classiques-arrivees__status-delayed {
color: var(--yellow); color: var(--yellow);
font-size: 30px; display: flex;
font-weight: 700; flex-direction: column;
line-height: 1.1; justify-content: center;
white-space: nowrap; align-items: flex-start;
font-family: "Achemine", sans-serif; 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 { .aff-classiques-arrivees__status-suppressed {
@@ -6,9 +6,23 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { fetchAllRows } from '../../../lib/fetchAllRows'; import { fetchAllRows } from '../../../lib/fetchAllRows';
import { getSourceFromPath } from '../../../services/railwayService'; 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'; import { getTrainLogoAndMeta } from '../../../utils/trainLogoHelper';
// Pictogramme Autocar SNCF / MOBIGO
const AutocarIcon = ({ size = '50px', color = '#ffffff', style = {} }) => (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill={color}
style={{ flexShrink: 0, ...style }}
>
<path d="M4 16c0 .88.39 1.67 1 2.22V20c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h8v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1.78c.61-.55 1-1.34 1-2.22V6c0-3.5-3.58-4-8-4s-8 .5-8 4v10zm3.5 1c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm1.5-6H6V6h12v5z" />
</svg>
);
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44) // Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => { const formatTimeNoSeconds = (v) => {
if (v == null) return ''; if (v == null) return '';
@@ -212,12 +226,8 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`; const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle); const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
if (!gerr && gdata && gdata.length) { if (!gerr && gdata && gdata.length) {
const exactMatch = gdata.find(g => { const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim(); const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
return n1 === n2;
});
const chosen = exactMatch || gdata[0];
fetchedType = chosen ? chosen.type : null; fetchedType = chosen ? chosen.type : null;
fetchedId = chosen ? chosen.id : null; fetchedId = chosen ? chosen.id : null;
} }
@@ -650,21 +660,20 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
const stopForStation = (stops || []).find(s => { const stopForStation = (stops || []).find(s => {
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null; 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 nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
const ns = normalizeName(nm || ''); return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
const target = normalizeName(targetStationName || '');
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
}) || null; }) || null;
horaire._status = displayStatus; let platformVal = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || horaire.quai || horaire.quai_reel || horaire.platform || horaire.voie || '';
horaire._displayTime = displayTime; if (!platformVal) {
// utiliser quai en priorité platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay)) || horaire.quai || horaire.quai_reel || horaire.platform || ''; }
horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
return horaire; return horaire;
}); });
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]); }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
useEffect(() => { useEffect(() => {
// Alternance toutes les 4 secondes (type+numéro ↔ statut) // Alternance toutes les 2 secondes (type+numéro ↔ statut / suppression)
const id = setInterval(() => setShowStatus(s => !s), 4000); const id = setInterval(() => setShowStatus(s => !s), 2000);
return () => clearInterval(id); return () => clearInterval(id);
}, []); }, []);
@@ -707,11 +716,13 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
} else { } else {
const formatted = (sncfData || []).map(t => { const formatted = (sncfData || []).map(t => {
const timeStr = t.heure_arrivee || t.heure_depart || t.time || ''; 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 { return {
...t, ...t,
_displayTime: formatTimeNoSeconds(timeStr), _displayTime: formatTimeNoSeconds(timeStr),
_displayPlatform: t.quai || t.voie || '', _displayPlatform: plat,
_arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr), _arrHM: arrHM,
_status: t._status || { _status: t._status || {
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"), label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
detail: '', detail: '',
@@ -720,8 +731,10 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
} }
}; };
}); });
setHoraires(formatted); const maxCount = defilementMode ? 20 : 10;
setAllHoraires(formatted); const limited = (formatted || []).slice(0, maxCount);
setHoraires(limited);
setAllHoraires(limited);
} }
} catch (e) { } catch (e) {
if (mounted) setError(e.message || String(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) // Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
let displayHoraires = parsedHoraires.slice(); let displayHoraires = parsedHoraires.slice();
if (gareDisplay) { if (gareDisplay) {
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
displayHoraires = displayHoraires.filter(h => { displayHoraires = displayHoraires.filter(h => {
// Exclure si la gare ciblée est la gare de départ // 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()); const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString();
if (departName === normalized) return false; if (matchStationNameStrict(departName, gareDisplay)) return false;
// Inclure si gare d'arrivée correspond // Inclure si gare d'arrivée correspond
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString()); const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
if (destName === normalized) return true; 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) // 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); const stopsList = getStops(h);
@@ -812,11 +824,11 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
} }
} }
for (const s of stopsList) { 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; if (!stopName) continue;
const isOrigine = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && minIdx != null && Number(s.index) === minIdx)); const isOrigine = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && minIdx != null && Number(s.index) === minIdx));
if (isOrigine) continue; if (isOrigine) continue;
if (stopName === normalized) return true; if (matchStationNameStrict(stopName, gareDisplay)) return true;
} }
} }
return false; return false;
@@ -889,7 +901,7 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
const stopsListForH = getStops(h); const stopsListForH = getStops(h);
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) { if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null; 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; const stop = byId || byName;
if (stop && (stop.heure_arrivee || stop.time || stop.heure)) raw = stop.heure_arrivee || stop.time || stop.heure; 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 // Création des lignes vides pour créer l'espace
const emptyRows = Array.from({ length: 10 }).map((_, i) => ({ _isEmpty: true, id: `empty-${i}` })); 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) // Liste de lignes à rendre (horaires + espace + horaires + espace en mode défilement pour boucle infinie)
const rowsToRender = defilementMode const rowsToRender = defilementMode
? [...horaires, ...emptyRows, ...horaires, ...emptyRows] ? [...limitedHoraires, ...emptyRows, ...limitedHoraires, ...emptyRows]
: horaires; : limitedHoraires;
// Calcul dynamique de l'animation CSS // Calcul dynamique de l'animation CSS
useEffect(() => { useEffect(() => {
@@ -1259,7 +1274,21 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations)); const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
const isSuppressed = status && status.label === 'supprimé'; const isSuppressed = status && status.label === 'supprimé';
const isDelayed = status && status.label && String(status.label).startsWith('retardé'); 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) : []; const stopsList = isExtended ? getStopsListForArrivees(horaire, gareDisplay || gareParam, normalizeName) : [];
@@ -1280,7 +1309,10 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
isSuppressed ? ( isSuppressed ? (
<div className="aff-classiques-arrivees__status-suppressed">supprimé</div> <div className="aff-classiques-arrivees__status-suppressed">supprimé</div>
) : isDelayed ? ( ) : isDelayed ? (
<div className="aff-classiques-arrivees__status-delayed">retard {status.delayMinutes} min.</div> <div className="aff-classiques-arrivees__status-delayed">
<div className="aff-classiques-arrivees__status-delayed-label">retardé</div>
<div className="aff-classiques-arrivees__status-delayed-time">+{status.delayMinutes || horaire.retard_minutes || 0} min.</div>
</div>
) : ( ) : (
<div className="aff-classiques-arrivees__status-ontime">à l'heure</div> <div className="aff-classiques-arrivees__status-ontime">à l'heure</div>
) )
@@ -1293,7 +1325,12 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
</div> </div>
<div className="aff-classiques-arrivees__col-time"> <div className="aff-classiques-arrivees__col-time">
<div className="aff-classiques-arrivees__time">{time}</div> <div
className="aff-classiques-arrivees__time"
style={{ visibility: isSuppressed && showStatus ? 'hidden' : 'visible' }}
>
{time}
</div>
</div> </div>
<div className="aff-classiques-arrivees__col-destination"> <div className="aff-classiques-arrivees__col-destination">
@@ -1303,7 +1340,11 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
<div className="aff-classiques-arrivees__col-platform"> <div className="aff-classiques-arrivees__col-platform">
{showPlatform ? ( {showPlatform ? (
<div className="aff-classiques-arrivees__platform-box"> <div className="aff-classiques-arrivees__platform-box">
<div className="aff-classiques-arrivees__platform-text">{cleanPlatform}</div> {isCar ? (
<AutocarIcon size="48px" />
) : (
<div className="aff-classiques-arrivees__platform-text">{cleanPlatform}</div>
)}
</div> </div>
) : null} ) : null}
</div> </div>
@@ -1346,7 +1387,10 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
isSuppressed ? ( isSuppressed ? (
<div className="aff-classiques-arrivees__status-suppressed">supprimé</div> <div className="aff-classiques-arrivees__status-suppressed">supprimé</div>
) : isDelayed ? ( ) : isDelayed ? (
<div className="aff-classiques-arrivees__status-delayed">retard {status.delayMinutes} min.</div> <div className="aff-classiques-arrivees__status-delayed">
<div className="aff-classiques-arrivees__status-delayed-label">retardé</div>
<div className="aff-classiques-arrivees__status-delayed-time">+{status.delayMinutes || horaire.retard_minutes || 0} min.</div>
</div>
) : ( ) : (
<div className="aff-classiques-arrivees__status-ontime">à l'heure</div> <div className="aff-classiques-arrivees__status-ontime">à l'heure</div>
) )
@@ -1360,7 +1404,12 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
{/* COL 3 : Heure (grande, jaune) */} {/* COL 3 : Heure (grande, jaune) */}
<div className="aff-classiques-arrivees__col-time"> <div className="aff-classiques-arrivees__col-time">
<div className="aff-classiques-arrivees__time">{time}</div> <div
className="aff-classiques-arrivees__time"
style={{ visibility: isSuppressed && showStatus ? 'hidden' : 'visible' }}
>
{time}
</div>
</div> </div>
{/* COL 4 : Provenance */} {/* COL 4 : Provenance */}
@@ -1372,7 +1421,11 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
<div className="aff-classiques-arrivees__col-platform"> <div className="aff-classiques-arrivees__col-platform">
{showPlatform ? ( {showPlatform ? (
<div className="aff-classiques-arrivees__platform-box"> <div className="aff-classiques-arrivees__platform-box">
<div className="aff-classiques-arrivees__platform-text">{cleanPlatform}</div> {isCar ? (
<AutocarIcon size="48px" />
) : (
<div className="aff-classiques-arrivees__platform-text">{cleanPlatform}</div>
)}
</div> </div>
) : null} ) : null}
</div> </div>
@@ -248,11 +248,25 @@
.aff-classiques-departs__status-delayed { .aff-classiques-departs__status-delayed {
color: var(--yellow); color: var(--yellow);
font-size: 30px; display: flex;
font-weight: 700; flex-direction: column;
line-height: 1.1; justify-content: center;
white-space: nowrap; align-items: flex-start;
font-family: "Achemine", sans-serif; 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 { .aff-classiques-departs__status-suppressed {
@@ -6,9 +6,23 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
import { fetchAllRows } from '../../../lib/fetchAllRows'; import { fetchAllRows } from '../../../lib/fetchAllRows';
import { getSourceFromPath } from '../../../services/railwayService'; 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'; import { getTrainLogoAndMeta } from '../../../utils/trainLogoHelper';
// Pictogramme Autocar SNCF / MOBIGO
const AutocarIcon = ({ size = '50px', color = '#ffffff', style = {} }) => (
<svg
viewBox="0 0 24 24"
width={size}
height={size}
fill={color}
style={{ flexShrink: 0, ...style }}
>
<path d="M4 16c0 .88.39 1.67 1 2.22V20c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1h8v1c0 .55.45 1 1 1h1c.55 0 1-.45 1-1v-1.78c.61-.55 1-1.34 1-2.22V6c0-3.5-3.58-4-8-4s-8 .5-8 4v10zm3.5 1c-.83 0-1.5-.67-1.5-1.5S6.67 14 7.5 14s1.5.67 1.5 1.5S8.33 17 7.5 17zm9 0c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm1.5-6H6V6h12v5z" />
</svg>
);
// Helper utilitaire : format HHhMM sans secondes (ex: 06h44) // Helper utilitaire : format HHhMM sans secondes (ex: 06h44)
const formatTimeNoSeconds = (v) => { const formatTimeNoSeconds = (v) => {
if (v == null) return ''; if (v == null) return '';
@@ -212,12 +226,8 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`; const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle); const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
if (!gerr && gdata && gdata.length) { if (!gerr && gdata && gdata.length) {
const exactMatch = gdata.find(g => { const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim(); const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
return n1 === n2;
});
const chosen = exactMatch || gdata[0];
fetchedType = chosen ? chosen.type : null; fetchedType = chosen ? chosen.type : null;
fetchedId = chosen ? chosen.id : null; fetchedId = chosen ? chosen.id : null;
} }
@@ -650,21 +660,20 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
const stopForStation = (stops || []).find(s => { const stopForStation = (stops || []).find(s => {
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null; 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 nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
const ns = normalizeName(nm || ''); return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
const target = normalizeName(targetStationName || '');
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
}) || null; }) || null;
horaire._status = displayStatus; let platformVal = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || horaire.quai || horaire.quai_reel || horaire.platform || horaire.voie || '';
horaire._displayTime = displayTime; if (!platformVal) {
// utiliser quai en priorité platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay)) || horaire.quai || horaire.quai_reel || horaire.platform || ''; }
horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
return horaire; return horaire;
}); });
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]); }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
useEffect(() => { useEffect(() => {
// Alternance toutes les 4 secondes (type+numéro ↔ statut) // Alternance toutes les 2 secondes (type+numéro ↔ statut / suppression)
const id = setInterval(() => setShowStatus(s => !s), 4000); const id = setInterval(() => setShowStatus(s => !s), 2000);
return () => clearInterval(id); return () => clearInterval(id);
}, []); }, []);
@@ -705,20 +714,26 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
if (sncfErr) { if (sncfErr) {
setError(sncfErr); setError(sncfErr);
} else { } else {
const formatted = (sncfData || []).map(t => ({ const formatted = (sncfData || []).map(t => {
...t, const depHM = (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart);
_displayTime: formatTimeNoSeconds(t.heure_depart), const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
_displayPlatform: t.quai || t.voie || '', return {
_depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart), ...t,
_status: t._status || { _displayTime: formatTimeNoSeconds(t.heure_depart),
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"), _displayPlatform: plat,
detail: '', _depHM: depHM,
delayMinutes: t.retard_minutes || null, _status: t._status || {
causeDetails: t.cause_retard || null label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
} detail: '',
})); delayMinutes: t.retard_minutes || null,
setHoraires(formatted); causeDetails: t.cause_retard || null
setAllHoraires(formatted); }
};
});
const maxCount = defilementMode ? 20 : 10;
const limited = (formatted || []).slice(0, maxCount);
setHoraires(limited);
setAllHoraires(limited);
} }
} catch (e) { } catch (e) {
if (mounted) setError(e.message || String(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) // Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
let displayHoraires = parsedHoraires.slice(); let displayHoraires = parsedHoraires.slice();
if (gareDisplay) { if (gareDisplay) {
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
displayHoraires = displayHoraires.filter(h => { displayHoraires = displayHoraires.filter(h => {
// Exclure si la gare ciblée est la gare d'arrivée finale // Exclure si la gare ciblée est la gare d'arrivée finale
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString()); const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
if (destName === normalized) return false; if (matchStationNameStrict(destName, gareDisplay)) return false;
// Inclure si gare de départ correspond // Inclure si gare de départ correspond
const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString()); const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString();
if (departName === normalized) return true; if (matchStationNameStrict(departName, gareDisplay)) return true;
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max) // Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max)
const stopsList = getStops(h); const stopsList = getStops(h);
@@ -809,11 +823,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
} }
} }
for (const s of stopsList) { 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; if (!stopName) continue;
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx)); const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
if (isTerminus) continue; if (isTerminus) continue;
if (stopName === normalized) return true; if (matchStationNameStrict(stopName, gareDisplay)) return true;
} }
} }
return false; return false;
@@ -886,7 +900,7 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
const stopsListForH = getStops(h); const stopsListForH = getStops(h);
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) { if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null; 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; const stop = byId || byName;
if (stop && (stop.heure_depart || stop.time || stop.heure)) raw = stop.heure_depart || stop.time || stop.heure; 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 // Création des lignes vides pour créer l'espace
const emptyRows = Array.from({ length: 10 }).map((_, i) => ({ _isEmpty: true, id: `empty-${i}` })); 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) // Liste de lignes à rendre (horaires + espace + horaires + espace en mode défilement pour boucle infinie)
const rowsToRender = defilementMode const rowsToRender = defilementMode
? [...horaires, ...emptyRows, ...horaires, ...emptyRows] ? [...limitedHoraires, ...emptyRows, ...limitedHoraires, ...emptyRows]
: horaires; : limitedHoraires;
// Calcul dynamique de l'animation CSS // Calcul dynamique de l'animation CSS
useEffect(() => { useEffect(() => {
@@ -1256,7 +1273,21 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations)); const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
const isSuppressed = status && status.label === 'supprimé'; const isSuppressed = status && status.label === 'supprimé';
const isDelayed = status && status.label && String(status.label).startsWith('retardé'); 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) : []; const stopsList = isExtended ? getStopsListForDeparts(horaire, gareDisplay || gareParam, normalizeName) : [];
@@ -1277,7 +1308,10 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
isSuppressed ? ( isSuppressed ? (
<div className="aff-classiques-departs__status-suppressed">supprimé</div> <div className="aff-classiques-departs__status-suppressed">supprimé</div>
) : isDelayed ? ( ) : isDelayed ? (
<div className="aff-classiques-departs__status-delayed">retard {status.delayMinutes} min.</div> <div className="aff-classiques-departs__status-delayed">
<div className="aff-classiques-departs__status-delayed-label">retardé</div>
<div className="aff-classiques-departs__status-delayed-time">+{status.delayMinutes || horaire.retard_minutes || 0} min.</div>
</div>
) : ( ) : (
<div className="aff-classiques-departs__status-ontime">à l'heure</div> <div className="aff-classiques-departs__status-ontime">à l'heure</div>
) )
@@ -1290,7 +1324,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
</div> </div>
<div className="aff-classiques-departs__col-time"> <div className="aff-classiques-departs__col-time">
<div className="aff-classiques-departs__time">{time}</div> <div
className="aff-classiques-departs__time"
style={{ visibility: isSuppressed && showStatus ? 'hidden' : 'visible' }}
>
{time}
</div>
</div> </div>
<div className="aff-classiques-departs__col-destination"> <div className="aff-classiques-departs__col-destination">
@@ -1300,7 +1339,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
<div className="aff-classiques-departs__col-platform"> <div className="aff-classiques-departs__col-platform">
{showPlatform ? ( {showPlatform ? (
<div className="aff-classiques-departs__platform-box"> <div className="aff-classiques-departs__platform-box">
<div className="aff-classiques-departs__platform-text">{cleanPlatform}</div> {isCar ? (
<AutocarIcon size="48px" />
) : (
<div className="aff-classiques-departs__platform-text">{cleanPlatform}</div>
)}
</div> </div>
) : null} ) : null}
</div> </div>
@@ -1343,7 +1386,10 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
isSuppressed ? ( isSuppressed ? (
<div className="aff-classiques-departs__status-suppressed">supprimé</div> <div className="aff-classiques-departs__status-suppressed">supprimé</div>
) : isDelayed ? ( ) : isDelayed ? (
<div className="aff-classiques-departs__status-delayed">retard {status.delayMinutes} min.</div> <div className="aff-classiques-departs__status-delayed">
<div className="aff-classiques-departs__status-delayed-label">retardé</div>
<div className="aff-classiques-departs__status-delayed-time">+{status.delayMinutes || horaire.retard_minutes || 0} min.</div>
</div>
) : ( ) : (
<div className="aff-classiques-departs__status-ontime">à l'heure</div> <div className="aff-classiques-departs__status-ontime">à l'heure</div>
) )
@@ -1357,7 +1403,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
{/* COL 3 : Heure (grande, jaune) */} {/* COL 3 : Heure (grande, jaune) */}
<div className="aff-classiques-departs__col-time"> <div className="aff-classiques-departs__col-time">
<div className="aff-classiques-departs__time">{time}</div> <div
className="aff-classiques-departs__time"
style={{ visibility: isSuppressed && showStatus ? 'hidden' : 'visible' }}
>
{time}
</div>
</div> </div>
{/* COL 4 : Destination */} {/* COL 4 : Destination */}
@@ -1369,7 +1420,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
<div className="aff-classiques-departs__col-platform"> <div className="aff-classiques-departs__col-platform">
{showPlatform ? ( {showPlatform ? (
<div className="aff-classiques-departs__platform-box"> <div className="aff-classiques-departs__platform-box">
<div className="aff-classiques-departs__platform-text">{cleanPlatform}</div> {isCar ? (
<AutocarIcon size="48px" />
) : (
<div className="aff-classiques-departs__platform-text">{cleanPlatform}</div>
)}
</div> </div>
) : null} ) : null}
</div> </div>
@@ -37,6 +37,11 @@ body, html {
z-index: 2; z-index: 2;
} }
.aff-classique-quai-arr__left--tgv {
bottom: 240px !important;
padding: 32px 48px 20px 48px !important;
}
/* Logo Dynamique (TER Bleu / Opérateur) */ /* Logo Dynamique (TER Bleu / Opérateur) */
.aff-classique-quai-arr__brand { .aff-classique-quai-arr__brand {
display: flex; display: flex;
@@ -65,6 +70,7 @@ body, html {
.aff-classique-quai-arr__time-row { .aff-classique-quai-arr__time-row {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
min-height: 100px;
margin-bottom: 8px; margin-bottom: 8px;
z-index: 3; z-index: 3;
} }
@@ -85,12 +91,18 @@ body, html {
margin-left: 20px; 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 { .aff-classique-quai-arr__status--suppressed {
color: #d32f2f !important; color: #d32f2f !important;
} }
.aff-classique-quai-arr__time--delayed,
.aff-classique-quai-arr__status--delayed { .aff-classique-quai-arr__status--delayed {
color: #f5a623 !important; color: #f5a623 !important;
} }
@@ -107,6 +119,22 @@ body, html {
word-break: break-word; 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) */ /* Info Train (Type + Numéro) */
.aff-classique-quai-arr__train-info { .aff-classique-quai-arr__train-info {
display: flex; display: flex;
@@ -173,6 +201,10 @@ body, html {
flex-direction: column; flex-direction: column;
} }
.aff-classique-quai-arr__right--tgv {
bottom: 240px !important;
}
.aff-classique-quai-arr__stops-wrapper { .aff-classique-quai-arr__stops-wrapper {
position: relative; position: relative;
width: 100%; width: 100%;
@@ -258,6 +290,74 @@ body, html {
font-size: 44px; 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) FOOTER & HORLOGE (Conforme Image 2)
========================================================= */ ========================================================= */
@@ -6,10 +6,34 @@ import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../../services/railwayService'; import { getSourceFromPath } from '../../../../services/railwayService';
import { getSncfTimetable } from '../../../../services/sncfApi'; import { getSncfTimetable } from '../../../../services/sncfApi';
import { matchStationNameStrict } from '../../../../services/gtfsStations';
import GareVideoprotection from '../../../../components/afficheurs/GareVideoprotection'; import GareVideoprotection from '../../../../components/afficheurs/GareVideoprotection';
import AttentifsEnsemble from '../../../../components/afficheurs/AttentifsEnsemble'; import AttentifsEnsemble from '../../../../components/afficheurs/AttentifsEnsemble';
import ClassiquesInformations from '../ClassiquesInformations'; import ClassiquesInformations from '../ClassiquesInformations';
import ClassiquesArrivees from '../ClassiquesArrivees'; import ClassiquesArrivees from '../ClassiquesArrivees';
import TgvCompositionQuai from '../../../../components/afficheurs/TgvCompositionQuai';
function TgvServiceIcons() {
return (
<div className="aff-classique-quai-arr__services">
{/* PMR / Fauteuil roulant */}
<svg width="30" height="30" viewBox="0 0 24 24" fill="#7e859b">
<path d="M12 2a2 2 0 100 4 2 2 0 000-4zm-2 6a1 1 0 00-1 1v6a1 1 0 001 1h4a1 1 0 001-1v-2a1 1 0 00-2 0v1h-2v-4h3a1 1 0 001-1V9a1 1 0 00-1-1H10zm-1 9a4 4 0 100 8 4 4 0 000-8zm0 2a2 2 0 110 4 2 2 0 010-4z"/>
</svg>
{/* Vélo */}
<svg width="34" height="30" viewBox="0 0 24 24" fill="none" stroke="#7e859b" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="5.5" cy="17.5" r="3.5"/>
<circle cx="18.5" cy="17.5" r="3.5"/>
<path d="M15 6a1 1 0 100-2 1 1 0 000 2zm-3 11.5L9 11.5l3-3.5h3.5m-6.5 7L12 8"/>
</svg>
{/* Bar / Verre */}
<svg width="28" height="30" viewBox="0 0 24 24" fill="none" stroke="#7e859b" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 4L12 14L20 4H4Z" fill="#7e859b" />
<path d="M12 14V21M8 21H16" />
</svg>
</div>
);
}
function DigitalClock() { function DigitalClock() {
const [now, setNow] = useState(new Date()); const [now, setNow] = useState(new Date());
@@ -131,6 +155,7 @@ export default function ClassiquesArriveesQuais() {
const [, setHoraireError] = useState(null); const [, setHoraireError] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date()); const [currentTime, setCurrentTime] = useState(new Date());
const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s) 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 pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase();
const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true'; const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true';
@@ -144,6 +169,12 @@ export default function ClassiquesArriveesQuais() {
return () => clearInterval(id); 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 // Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai
useEffect(() => { useEffect(() => {
if (selectedHoraire) return; if (selectedHoraire) return;
@@ -315,14 +346,29 @@ export default function ClassiquesArriveesQuais() {
if (sncfData && sncfData.length > 0) { if (sncfData && sncfData.length > 0) {
let matchingTrains = sncfData; let matchingTrains = sncfData;
if (targetQuaiClean) { 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]; const train = matchingTrains[0];
if (train) { if (train) {
const arrHM = parseTimeHM(train.heure_arrivee || train.time || train.heure); 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 = { chosenTrain = {
horaire: { horaire: {
...train, ...train,
quai: trainPlatform,
quai_reel: trainPlatform,
voie: trainPlatform,
_displayPlatform: trainPlatform,
_status: train._status || { _status: train._status || {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"), label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
detail: '', detail: '',
@@ -330,10 +376,11 @@ export default function ClassiquesArriveesQuais() {
causeDetails: train.cause_retard || null causeDetails: train.cause_retard || null
} }
}, },
partner: partner || null,
stop: { stop: {
gare_nom: train.gare_arrivee_nom || gareDisplay || gareParam, gare_nom: train.gare_arrivee_nom || gareDisplay || gareParam,
heure_arrivee: train.heure_arrivee || train.time || train.heure, heure_arrivee: train.heure_arrivee || train.time || train.heure,
quai: train.quai || train.voie || quaiParam quai: trainPlatform
}, },
arrHM: arrHM != null ? arrHM : 0, arrHM: arrHM != null ? arrHM : 0,
rawTime: train.heure_arrivee || train.time || train.heure rawTime: train.heure_arrivee || train.time || train.heure
@@ -353,13 +400,14 @@ export default function ClassiquesArriveesQuais() {
} else if (gareDisplay) { } else if (gareDisplay) {
const { data: gdata } = await supabase.from('gares').select('id,nom').ilike('nom', `%${gareDisplay}%`); const { data: gdata } = await supabase.from('gares').select('id,nom').ilike('nom', `%${gareDisplay}%`);
if (gdata && gdata.length) { 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('*')); const { data: all, error } = await fetchAllRows(supabase.from('horaires').select('*'));
if (!error && Array.isArray(all) && all.length > 0) { if (!error && Array.isArray(all) && all.length > 0) {
const normalizedTarget = gareDisplay ? normalizeName(gareDisplay) : null;
const now = new Date(); const now = new Date();
const tomorrow = new Date(now); const tomorrow = new Date(now);
tomorrow.setDate(now.getDate() + 1); tomorrow.setDate(now.getDate() + 1);
@@ -375,14 +423,14 @@ export default function ClassiquesArriveesQuais() {
const stops = getStops(h); const stops = getStops(h);
let stopMatch = null; let stopMatch = null;
if (normalizedTarget) { if (gareDisplay) {
const departNameNorm = normalizeName(h.gare_depart_nom || h.gare_depart || ''); const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
if (departNameNorm && departNameNorm === normalizedTarget) { if (matchStationNameStrict(departName, gareDisplay)) {
// Target is the departure origin -> not an arrival here // Target is the departure origin -> not an arrival here
continue; continue;
} }
const arriveeName = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || ''); const arriveeName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
if (arriveeName && arriveeName === normalizedTarget) { if (matchStationNameStrict(arriveeName, gareDisplay)) {
stopMatch = { stopMatch = {
heure_arrivee: h.heure_arrivee || h.time || h.heure, heure_arrivee: h.heure_arrivee || h.time || h.heure,
gare_nom: h.gare_arrivee_nom || h.gare_arrivee, gare_nom: h.gare_arrivee_nom || h.gare_arrivee,
@@ -391,8 +439,8 @@ export default function ClassiquesArriveesQuais() {
} }
if (!stopMatch && Array.isArray(stops)) { if (!stopMatch && Array.isArray(stops)) {
for (const s of stops) { for (const s of stops) {
const sName = normalizeName(typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || '')); const sName = (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || '')).toString();
if (sName === normalizedTarget) { if (matchStationNameStrict(sName, gareDisplay)) {
stopMatch = s; stopMatch = s;
break; break;
} }
@@ -408,7 +456,11 @@ export default function ClassiquesArriveesQuais() {
if (!stopMatch) continue; 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); const platClean = cleanPlatform(platform);
if (targetQuaiClean) { 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({ candidates.push({
horaire: h, horaire: h,
stop: stopMatch, stop: stopMatch,
@@ -448,7 +496,16 @@ export default function ClassiquesArriveesQuais() {
: (filteredCandidates.length > 0 ? filteredCandidates.sort((a, b) => a.arrHM - b.arrHM)[0] : null)); : (filteredCandidates.length > 0 ? filteredCandidates.sort((a, b) => a.arrHM - b.arrHM)[0] : null));
if (chosen) { 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) { } catch (err) {
@@ -501,8 +558,7 @@ export default function ClassiquesArriveesQuais() {
// Filtrer les arrêts jusqu'à la gare courante // Filtrer les arrêts jusqu'à la gare courante
if (gareDisplay && stopsList.length > 0) { if (gareDisplay && stopsList.length > 0) {
const normCur = normalizeName(gareDisplay); const foundIdx = stopsList.findIndex(s => matchStationNameStrict(s, gareDisplay));
const foundIdx = stopsList.findIndex(s => normalizeName(s) === normCur);
if (foundIdx > 0) { if (foundIdx > 0) {
stopsList = stopsList.slice(0, foundIdx + 1); stopsList = stopsList.slice(0, foundIdx + 1);
} }
@@ -510,9 +566,8 @@ export default function ClassiquesArriveesQuais() {
// Garantir que le terminus est présent en dernier // Garantir que le terminus est présent en dernier
if (destinationName) { if (destinationName) {
const destNorm = normalizeName(destinationName); const lastStop = stopsList.length > 0 ? stopsList[stopsList.length - 1] : '';
const lastNorm = stopsList.length > 0 ? normalizeName(stopsList[stopsList.length - 1]) : ''; if (!matchStationNameStrict(lastStop, destinationName)) {
if (lastNorm !== destNorm) {
stopsList.push(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 ( return (
<div className="aff-classique-quai-arr__container"> <div className="aff-classique-quai-arr__container">
{/* PANNEAU GAUCHE : BLANC (Conforme Image 2) */} {/* PANNEAU GAUCHE : BLANC (Conforme Image 2) */}
<div className="aff-classique-quai-arr__left"> <div className={`aff-classique-quai-arr__left ${isTgv ? 'aff-classique-quai-arr__left--tgv' : ''}`}>
{/* Logo Couleur Dynamique */} {/* Logo Couleur Dynamique */}
<div className="aff-classique-quai-arr__brand"> <div className="aff-classique-quai-arr__brand">
<img src={brand.logo} alt={brand.label} className="aff-classique-quai-arr__brand-logo" /> <img src={brand.logo} alt={brand.label} className="aff-classique-quai-arr__brand-logo" />
@@ -559,68 +668,164 @@ export default function ClassiquesArriveesQuais() {
{/* Heure et Statut */} {/* Heure et Statut */}
<div className="aff-classique-quai-arr__time-row"> <div className="aff-classique-quai-arr__time-row">
<span className={`aff-classique-quai-arr__time ${isSuppressed ? 'aff-classique-quai-arr__time--suppressed' : ''} ${isDelayed ? 'aff-classique-quai-arr__time--delayed' : ''}`}> {(isSuppressed || isDelayed) ? (
{formattedTime || '--:--'} showStatusAlt ? (
</span> <span className={`aff-classique-quai-arr__status-alt ${isSuppressed ? 'aff-classique-quai-arr__status--suppressed' : 'aff-classique-quai-arr__status--delayed'}`}>
<span className={`aff-classique-quai-arr__status ${isSuppressed ? 'aff-classique-quai-arr__status--suppressed' : ''} ${isDelayed ? 'aff-classique-quai-arr__status--delayed' : ''}`}> {displayStatusText}
{displayStatusText} </span>
</span> ) : (
<span className="aff-classique-quai-arr__time">
{formattedTime || '--:--'}
</span>
)
) : (
<>
<span className="aff-classique-quai-arr__time">
{formattedTime || '--:--'}
</span>
<span className="aff-classique-quai-arr__status">
{displayStatusText}
</span>
</>
)}
</div> </div>
{/* Destination / Gare d'Arrivée Principale */} {/* Destination / Gare d'Arrivée Principale */}
<div className="aff-classique-quai-arr__destination"> <div className={`aff-classique-quai-arr__destination ${isDifferentDest ? 'aff-classique-quai-arr__destination--multi' : ''}`}>
{destinationName || 'Aucune arrivée prévue'} {destinationName1 || 'Aucune arrivée prévue'}
</div> </div>
{/* Type de Train & Numéro */} {/* Type de Train & Numéro 1 */}
<div className="aff-classique-quai-arr__train-info"> <div className="aff-classique-quai-arr__train-info">
{trainType && <span className="aff-classique-quai-arr__train-type">{trainType}</span>} {trainType1 && <span className="aff-classique-quai-arr__train-type">{trainType1}</span>}
{trainNumber && <span className="aff-classique-quai-arr__train-number">{trainNumber}</span>} {trainNumber1 && <span className="aff-classique-quai-arr__train-number">{trainNumber1}</span>}
</div> </div>
{/* Destination 2 si multi-tranche / destinations différentes */}
{isDifferentDest && (
<>
<div className="aff-classique-quai-arr__destination aff-classique-quai-arr__destination--multi" style={{ marginTop: '16px' }}>
{destinationName2}
</div>
<div className="aff-classique-quai-arr__train-info">
{trainType2 && <span className="aff-classique-quai-arr__train-type">{trainType2}</span>}
{trainNumber2 && <span className="aff-classique-quai-arr__train-number">{trainNumber2}</span>}
</div>
</>
)}
{/* Si même destination mais 2 numéros couplés */}
{!isDifferentDest && trainNumber2 && trainNumber2 !== trainNumber1 && (
<div className="aff-classique-quai-arr__train-info" style={{ marginTop: '6px' }}>
{trainType2 && <span className="aff-classique-quai-arr__train-type">{trainType2}</span>}
<span className="aff-classique-quai-arr__train-number">{trainNumber2}</span>
</div>
)}
{/* Pictogrammes de services pour TGV (PMR, Vélo, Bar) */}
{isTgv && <TgvServiceIcons />}
{/* Filigrane Vertical "arrivée" géant (orienté bas vers haut) */} {/* Filigrane Vertical "arrivée" géant (orienté bas vers haut) */}
<div className="aff-classique-quai-arr__watermark" aria-hidden="true"> <div className="aff-classique-quai-arr__watermark" aria-hidden="true">
<span>arrivée</span> <span>arrivée</span>
</div> </div>
</div> </div>
{/* PANNEAU DROIT : VERT FORÊT AVEC DESSERTE (Conforme Image 2) */} {/* PANNEAU DROIT : VERT FORÊT AVEC DESSERTE (Conforme Image 2 & Fourche) */}
<div className="aff-classique-quai-arr__right"> <div className={`aff-classique-quai-arr__right ${isTgv ? 'aff-classique-quai-arr__right--tgv' : ''}`}>
<div className="aff-classique-quai-arr__stops-wrapper"> <div className="aff-classique-quai-arr__stops-wrapper">
{stopsList.length > 0 ? ( {isDifferentDest && commonStops.length > 0 ? (
<ul className="aff-classique-quai-arr__stops-list"> /* AFFICHAGE FOURCHE / BIFURCATION */
{stopsList.map((stopName, idx) => { <div className="aff-classique-quai-arr__fork-container">
const isLast = idx === stopsList.length - 1; {/* Tronc commun */}
return ( <ul className="aff-classique-quai-arr__stops-list">
<li {commonStops.map((stopName, idx) => (
key={idx} <li key={idx} className="aff-classique-quai-arr__stop-item">
className={`aff-classique-quai-arr__stop-item ${isLast ? 'aff-classique-quai-arr__stop-item--dest' : ''}`}
>
<span className="aff-classique-quai-arr__bullet" /> <span className="aff-classique-quai-arr__bullet" />
<span className="aff-classique-quai-arr__stop-name"> <span className="aff-classique-quai-arr__stop-name">{stopName}</span>
{stopName}
</span>
</li> </li>
); ))}
})} </ul>
</ul>
) : ( {/* Ligne d'embranchement avec courbe */}
<div style={{ color: '#ffffff', fontSize: '38px', fontWeight: 600, padding: '40px' }}> <div className="aff-classique-quai-arr__fork-split-row">
Veuillez consulter les écrans du hall de la gare. <div className="aff-classique-quai-arr__fork-curve-rail" />
<div className="aff-classique-quai-arr__fork-branch-right">
<div className="aff-classique-quai-arr__fork-coaches-label">voitures 11 à 18</div>
<div className="aff-classique-quai-arr__fork-terminus-box">
<span className="aff-classique-quai-arr__bullet" style={{ position: 'static', margin: 0 }} />
<span className="aff-classique-quai-arr__fork-terminus-name">
{branch2Stops[branch2Stops.length - 1] || destinationName2}
</span>
</div>
</div>
</div>
{/* Branche principale continuant vers terminus 1 */}
{branch1Stops.length > 0 && (
<ul className="aff-classique-quai-arr__stops-list" style={{ marginTop: '8px' }}>
{branch1Stops.map((stopName, idx) => {
const isLast = idx === branch1Stops.length - 1;
return (
<li
key={idx}
className={`aff-classique-quai-arr__stop-item ${isLast ? 'aff-classique-quai-arr__stop-item--dest' : ''}`}
>
<span className="aff-classique-quai-arr__bullet" />
<span className="aff-classique-quai-arr__stop-name">{stopName}</span>
</li>
);
})}
</ul>
)}
</div> </div>
) : (
/* AFFICHAGE STANDARD DESSERTE VERTICALE */
stopsList.length > 0 ? (
<ul className="aff-classique-quai-arr__stops-list">
{stopsList.map((stopName, idx) => {
const isLast = idx === stopsList.length - 1;
return (
<li
key={idx}
className={`aff-classique-quai-arr__stop-item ${isLast ? 'aff-classique-quai-arr__stop-item--dest' : ''}`}
>
<span className="aff-classique-quai-arr__bullet" />
<span className="aff-classique-quai-arr__stop-name">
{stopName}
</span>
</li>
);
})}
</ul>
) : (
<div style={{ color: '#ffffff', fontSize: '38px', fontWeight: 600, padding: '40px' }}>
Veuillez consulter les écrans du hall de la gare.
</div>
)
)} )}
</div> </div>
</div> </div>
{/* BANDEAU BAS : PLEINE LARGEUR & HORLOGE (Conforme Image 2) */} {/* BANDEAU BAS : TGV COMPOSITION OU BANDEAU STANDARD */}
<div className="aff-classique-quai-arr__footer"> {isTgv ? (
<div className="aff-classique-quai-arr__footer-track"> <TgvCompositionQuai
{quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai ? `Voie ${cleanPlatform(horaire.quai).toUpperCase()}` : '')} platform={quaiParam || horaire?.quai || horaire?.quai_reel || horaire?.voie || horaire?._displayPlatform}
destination={destinationName1}
destination2={isDifferentDest ? destinationName2 : destinationName1}
composition={horaire?.composition}
marqueeMessage={horaire?._status?.causeDetails || horaire?.cause_retard || (isSuppressed ? `TRAIN SUPPRIMÉ - MERCI DE VOTRE COMPRÉHENSION` : (isDelayed ? `TRAIN RETARDÉ DE ${delayMinutes} MIN - MERCI DE VOTRE COMPRÉHENSION` : ''))}
/>
) : (
<div className="aff-classique-quai-arr__footer">
<div className="aff-classique-quai-arr__footer-track">
{quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai || horaire?.voie || horaire?._displayPlatform ? `Voie ${cleanPlatform(horaire.quai || horaire.voie || horaire._displayPlatform).toUpperCase()}` : '')}
</div>
<div className="aff-classique-quai-arr__footer-clock">
<DigitalClock />
</div>
</div> </div>
<div className="aff-classique-quai-arr__footer-clock"> )}
<DigitalClock />
</div>
</div>
</div> </div>
); );
} }
@@ -37,6 +37,11 @@ body, html {
z-index: 2; z-index: 2;
} }
.aff-classique-quai-dep__left--tgv {
bottom: 240px !important;
padding: 32px 48px 20px 48px !important;
}
/* Logo Dynamique */ /* Logo Dynamique */
.aff-classique-quai-dep__brand { .aff-classique-quai-dep__brand {
display: flex; display: flex;
@@ -65,6 +70,7 @@ body, html {
.aff-classique-quai-dep__time-row { .aff-classique-quai-dep__time-row {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
min-height: 100px;
margin-bottom: 8px; margin-bottom: 8px;
z-index: 3; z-index: 3;
} }
@@ -85,12 +91,18 @@ body, html {
margin-left: 20px; 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 { .aff-classique-quai-dep__status--suppressed {
color: #d32f2f !important; color: #d32f2f !important;
} }
.aff-classique-quai-dep__time--delayed,
.aff-classique-quai-dep__status--delayed { .aff-classique-quai-dep__status--delayed {
color: #f5a623 !important; color: #f5a623 !important;
} }
@@ -107,6 +119,22 @@ body, html {
word-break: break-word; 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) */ /* Info Train (Type + Numéro) */
.aff-classique-quai-dep__train-info { .aff-classique-quai-dep__train-info {
display: flex; display: flex;
@@ -173,6 +201,10 @@ body, html {
flex-direction: column; flex-direction: column;
} }
.aff-classique-quai-dep__right--tgv {
bottom: 240px !important;
}
.aff-classique-quai-dep__stops-wrapper { .aff-classique-quai-dep__stops-wrapper {
position: relative; position: relative;
width: 100%; width: 100%;
@@ -258,6 +290,74 @@ body, html {
font-size: 44px; 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) FOOTER & HORLOGE (Conforme Image 1)
========================================================= */ ========================================================= */
@@ -6,11 +6,35 @@ import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce'; import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
import { getSourceFromPath } from '../../../../services/railwayService'; import { getSourceFromPath } from '../../../../services/railwayService';
import { getSncfTimetable } from '../../../../services/sncfApi'; import { getSncfTimetable } from '../../../../services/sncfApi';
import { matchStationNameStrict } from '../../../../services/gtfsStations';
import GareVideoprotection from '../../../../components/afficheurs/GareVideoprotection'; import GareVideoprotection from '../../../../components/afficheurs/GareVideoprotection';
import AttentifsEnsemble from '../../../../components/afficheurs/AttentifsEnsemble'; import AttentifsEnsemble from '../../../../components/afficheurs/AttentifsEnsemble';
import DepartEnCoursClassique from '../../../../components/afficheurs/DepartEnCoursClassique'; import DepartEnCoursClassique from '../../../../components/afficheurs/DepartEnCoursClassique';
import ClassiquesInformations from '../ClassiquesInformations'; import ClassiquesInformations from '../ClassiquesInformations';
import ClassiquesDeparts from '../ClassiquesDeparts'; import ClassiquesDeparts from '../ClassiquesDeparts';
import TgvCompositionQuai from '../../../../components/afficheurs/TgvCompositionQuai';
function TgvServiceIcons() {
return (
<div className="aff-classique-quai-dep__services">
{/* PMR / Fauteuil roulant */}
<svg width="30" height="30" viewBox="0 0 24 24" fill="#7e859b">
<path d="M12 2a2 2 0 100 4 2 2 0 000-4zm-2 6a1 1 0 00-1 1v6a1 1 0 001 1h4a1 1 0 001-1v-2a1 1 0 00-2 0v1h-2v-4h3a1 1 0 001-1V9a1 1 0 00-1-1H10zm-1 9a4 4 0 100 8 4 4 0 000-8zm0 2a2 2 0 110 4 2 2 0 010-4z"/>
</svg>
{/* Vélo */}
<svg width="34" height="30" viewBox="0 0 24 24" fill="none" stroke="#7e859b" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="5.5" cy="17.5" r="3.5"/>
<circle cx="18.5" cy="17.5" r="3.5"/>
<path d="M15 6a1 1 0 100-2 1 1 0 000 2zm-3 11.5L9 11.5l3-3.5h3.5m-6.5 7L12 8"/>
</svg>
{/* Bar / Verre */}
<svg width="28" height="30" viewBox="0 0 24 24" fill="none" stroke="#7e859b" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 4L12 14L20 4H4Z" fill="#7e859b" />
<path d="M12 14V21M8 21H16" />
</svg>
</div>
);
}
function DigitalClock() { function DigitalClock() {
const [now, setNow] = useState(new Date()); const [now, setNow] = useState(new Date());
@@ -132,6 +156,7 @@ export default function ClassiquesDepartsQuais() {
const [, setHoraireError] = useState(null); const [, setHoraireError] = useState(null);
const [currentTime, setCurrentTime] = useState(new Date()); const [currentTime, setCurrentTime] = useState(new Date());
const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s) 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 pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase();
const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true'; const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true';
@@ -147,6 +172,12 @@ export default function ClassiquesDepartsQuais() {
return () => clearInterval(id); 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 // Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai
useEffect(() => { useEffect(() => {
if (selectedHoraire) return; if (selectedHoraire) return;
@@ -317,14 +348,29 @@ export default function ClassiquesDepartsQuais() {
if (sncfData && sncfData.length > 0) { if (sncfData && sncfData.length > 0) {
let matchingTrains = sncfData; let matchingTrains = sncfData;
if (targetQuaiClean) { 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]; const train = matchingTrains[0];
if (train) { if (train) {
const depHM = parseTimeHM(train.heure_depart || train.time || train.heure); 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 = { chosenTrain = {
horaire: { horaire: {
...train, ...train,
quai: trainPlatform,
quai_reel: trainPlatform,
voie: trainPlatform,
_displayPlatform: trainPlatform,
_status: train._status || { _status: train._status || {
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"), label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
detail: '', detail: '',
@@ -332,10 +378,11 @@ export default function ClassiquesDepartsQuais() {
causeDetails: train.cause_retard || null causeDetails: train.cause_retard || null
} }
}, },
partner: partner || null,
stop: { stop: {
gare_nom: train.gare_depart_nom || gareDisplay || gareParam, gare_nom: train.gare_depart_nom || gareDisplay || gareParam,
heure_depart: train.heure_depart || train.time || train.heure, heure_depart: train.heure_depart || train.time || train.heure,
quai: train.quai || train.voie || quaiParam quai: trainPlatform
}, },
depHM: depHM != null ? depHM : 0, depHM: depHM != null ? depHM : 0,
rawTime: train.heure_depart || train.time || train.heure rawTime: train.heure_depart || train.time || train.heure
@@ -355,13 +402,14 @@ export default function ClassiquesDepartsQuais() {
} else if (gareDisplay) { } else if (gareDisplay) {
const { data: gdata } = await supabase.from('gares').select('id,nom').ilike('nom', `%${gareDisplay}%`); const { data: gdata } = await supabase.from('gares').select('id,nom').ilike('nom', `%${gareDisplay}%`);
if (gdata && gdata.length) { 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('*')); const { data: all, error } = await fetchAllRows(supabase.from('horaires').select('*'));
if (!error && Array.isArray(all) && all.length > 0) { if (!error && Array.isArray(all) && all.length > 0) {
const normalizedTarget = gareDisplay ? normalizeName(gareDisplay) : null;
const now = new Date(); const now = new Date();
const tomorrow = new Date(now); const tomorrow = new Date(now);
tomorrow.setDate(now.getDate() + 1); tomorrow.setDate(now.getDate() + 1);
@@ -377,14 +425,14 @@ export default function ClassiquesDepartsQuais() {
const stops = getStops(h); const stops = getStops(h);
let stopMatch = null; let stopMatch = null;
if (normalizedTarget) { if (gareDisplay) {
const destNameNorm = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || ''); const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
if (destNameNorm && destNameNorm === normalizedTarget) { if (matchStationNameStrict(destName, gareDisplay)) {
// Destination finale -> terminus ici, pas un départ // Destination finale -> terminus ici, pas un départ
continue; continue;
} }
const departName = normalizeName(h.gare_depart_nom || h.gare_depart || ''); const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
if (departName && departName === normalizedTarget) { if (matchStationNameStrict(departName, gareDisplay)) {
stopMatch = { stopMatch = {
heure_depart: h.heure_depart || h.time || h.heure, heure_depart: h.heure_depart || h.time || h.heure,
gare_nom: h.gare_depart_nom || h.gare_depart, gare_nom: h.gare_depart_nom || h.gare_depart,
@@ -393,8 +441,8 @@ export default function ClassiquesDepartsQuais() {
} }
if (!stopMatch && Array.isArray(stops)) { if (!stopMatch && Array.isArray(stops)) {
for (const s of stops) { for (const s of stops) {
const sName = normalizeName(typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || '')); const sName = (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || '')).toString();
if (sName === normalizedTarget) { if (matchStationNameStrict(sName, gareDisplay)) {
stopMatch = s; stopMatch = s;
break; break;
} }
@@ -410,7 +458,11 @@ export default function ClassiquesDepartsQuais() {
if (!stopMatch) continue; 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); const platClean = cleanPlatform(platform);
if (targetQuaiClean) { 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({ candidates.push({
horaire: h, horaire: h,
stop: stopMatch, stop: stopMatch,
@@ -450,7 +498,16 @@ export default function ClassiquesDepartsQuais() {
: (filteredCandidates.length > 0 ? filteredCandidates.sort((a, b) => a.depHM - b.depHM)[0] : null)); : (filteredCandidates.length > 0 ? filteredCandidates.sort((a, b) => a.depHM - b.depHM)[0] : null));
if (chosen) { 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) { } catch (err) {
@@ -503,8 +560,7 @@ export default function ClassiquesDepartsQuais() {
// Filtrer les arrêts à partir de la gare courante // Filtrer les arrêts à partir de la gare courante
if (gareDisplay && stopsList.length > 0) { if (gareDisplay && stopsList.length > 0) {
const normCur = normalizeName(gareDisplay); const foundIdx = stopsList.findIndex(s => matchStationNameStrict(s, gareDisplay));
const foundIdx = stopsList.findIndex(s => normalizeName(s) === normCur);
if (foundIdx >= 0 && foundIdx < stopsList.length - 1) { if (foundIdx >= 0 && foundIdx < stopsList.length - 1) {
stopsList = stopsList.slice(foundIdx + 1); stopsList = stopsList.slice(foundIdx + 1);
} }
@@ -512,9 +568,8 @@ export default function ClassiquesDepartsQuais() {
// Garantir que le terminus est présent en dernier // Garantir que le terminus est présent en dernier
if (destinationName) { if (destinationName) {
const destNorm = normalizeName(destinationName); const lastStop = stopsList.length > 0 ? stopsList[stopsList.length - 1] : '';
const lastNorm = stopsList.length > 0 ? normalizeName(stopsList[stopsList.length - 1]) : ''; if (!matchStationNameStrict(lastStop, destinationName)) {
if (lastNorm !== destNorm) {
stopsList.push(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 ( return (
<div className="aff-classique-quai-dep__container"> <div className="aff-classique-quai-dep__container">
{/* PANNEAU GAUCHE : BLANC (Conforme Image 1) */} {/* PANNEAU GAUCHE : BLANC (Conforme Image 1 & Image 2) */}
<div className="aff-classique-quai-dep__left"> <div className={`aff-classique-quai-dep__left ${isTgv ? 'aff-classique-quai-dep__left--tgv' : ''}`}>
{/* Logo Couleur Dynamique */} {/* Logo Couleur Dynamique */}
<div className="aff-classique-quai-dep__brand"> <div className="aff-classique-quai-dep__brand">
<img src={brand.logo} alt={brand.label} className="aff-classique-quai-dep__brand-logo" /> <img src={brand.logo} alt={brand.label} className="aff-classique-quai-dep__brand-logo" />
@@ -581,68 +690,164 @@ export default function ClassiquesDepartsQuais() {
{/* Heure et Statut */} {/* Heure et Statut */}
<div className="aff-classique-quai-dep__time-row"> <div className="aff-classique-quai-dep__time-row">
<span className={`aff-classique-quai-dep__time ${isSuppressed ? 'aff-classique-quai-dep__time--suppressed' : ''} ${isDelayed ? 'aff-classique-quai-dep__time--delayed' : ''}`}> {(isSuppressed || isDelayed) ? (
{formattedTime || '--:--'} showStatusAlt ? (
</span> <span className={`aff-classique-quai-dep__status-alt ${isSuppressed ? 'aff-classique-quai-dep__status--suppressed' : 'aff-classique-quai-dep__status--delayed'}`}>
<span className={`aff-classique-quai-dep__status ${isSuppressed ? 'aff-classique-quai-dep__status--suppressed' : ''} ${isDelayed ? 'aff-classique-quai-dep__status--delayed' : ''}`}> {displayStatusText}
{displayStatusText} </span>
</span> ) : (
<span className="aff-classique-quai-dep__time">
{formattedTime || '--:--'}
</span>
)
) : (
<>
<span className="aff-classique-quai-dep__time">
{formattedTime || '--:--'}
</span>
<span className="aff-classique-quai-dep__status">
{displayStatusText}
</span>
</>
)}
</div> </div>
{/* Destination Principale */} {/* Destination Principale */}
<div className="aff-classique-quai-dep__destination"> <div className={`aff-classique-quai-dep__destination ${isDifferentDest ? 'aff-classique-quai-dep__destination--multi' : ''}`}>
{destinationName || 'Aucun départ prévu'} {destinationName1 || 'Aucun départ prévu'}
</div> </div>
{/* Type de Train & Numéro */} {/* Type de Train & Numéro 1 */}
<div className="aff-classique-quai-dep__train-info"> <div className="aff-classique-quai-dep__train-info">
{trainType && <span className="aff-classique-quai-dep__train-type">{trainType}</span>} {trainType1 && <span className="aff-classique-quai-dep__train-type">{trainType1}</span>}
{trainNumber && <span className="aff-classique-quai-dep__train-number">{trainNumber}</span>} {trainNumber1 && <span className="aff-classique-quai-dep__train-number">{trainNumber1}</span>}
</div> </div>
{/* Destination 2 si multi-tranche / destinations différentes (ex: Nice-Ville ou Bruxelles Midi) */}
{isDifferentDest && (
<>
<div className="aff-classique-quai-dep__destination aff-classique-quai-dep__destination--multi" style={{ marginTop: '16px' }}>
{destinationName2}
</div>
<div className="aff-classique-quai-dep__train-info">
{trainType2 && <span className="aff-classique-quai-dep__train-type">{trainType2}</span>}
{trainNumber2 && <span className="aff-classique-quai-dep__train-number">{trainNumber2}</span>}
</div>
</>
)}
{/* Si même destination mais 2 numéros couplés (ex: 2 TGV départ 16h17 vers Paris) */}
{!isDifferentDest && trainNumber2 && trainNumber2 !== trainNumber1 && (
<div className="aff-classique-quai-dep__train-info" style={{ marginTop: '6px' }}>
{trainType2 && <span className="aff-classique-quai-dep__train-type">{trainType2}</span>}
<span className="aff-classique-quai-dep__train-number">{trainNumber2}</span>
</div>
)}
{/* Pictogrammes de services pour TGV (PMR, Vélo, Bar) */}
{isTgv && <TgvServiceIcons />}
{/* Filigrane Vertical "départ" géant (orienté bas vers haut) */} {/* Filigrane Vertical "départ" géant (orienté bas vers haut) */}
<div className="aff-classique-quai-dep__watermark" aria-hidden="true"> <div className="aff-classique-quai-dep__watermark" aria-hidden="true">
<span>départ</span> <span>départ</span>
</div> </div>
</div> </div>
{/* PANNEAU DROIT : BLEU NUIT AVEC DESSERTE (Conforme Image 1) */} {/* PANNEAU DROIT : BLEU NUIT AVEC DESSERTE (Conforme Image 1 & Fourche Image 2) */}
<div className="aff-classique-quai-dep__right"> <div className={`aff-classique-quai-dep__right ${isTgv ? 'aff-classique-quai-dep__right--tgv' : ''}`}>
<div className="aff-classique-quai-dep__stops-wrapper"> <div className="aff-classique-quai-dep__stops-wrapper">
{stopsList.length > 0 ? ( {isDifferentDest && commonStops.length > 0 ? (
<ul className="aff-classique-quai-dep__stops-list"> /* AFFICHAGE FOURCHE / BIFURCATION (Conforme Image 2) */
{stopsList.map((stopName, idx) => { <div className="aff-classique-quai-dep__fork-container">
const isLast = idx === stopsList.length - 1; {/* Tronc commun */}
return ( <ul className="aff-classique-quai-dep__stops-list">
<li {commonStops.map((stopName, idx) => (
key={idx} <li key={idx} className="aff-classique-quai-dep__stop-item">
className={`aff-classique-quai-dep__stop-item ${isLast ? 'aff-classique-quai-dep__stop-item--dest' : ''}`}
>
<span className="aff-classique-quai-dep__bullet" /> <span className="aff-classique-quai-dep__bullet" />
<span className="aff-classique-quai-dep__stop-name"> <span className="aff-classique-quai-dep__stop-name">{stopName}</span>
{stopName}
</span>
</li> </li>
); ))}
})} </ul>
</ul>
) : ( {/* Ligne d'embranchement avec courbe */}
<div style={{ color: '#ffffff', fontSize: '38px', fontWeight: 600, padding: '40px' }}> <div className="aff-classique-quai-dep__fork-split-row">
Veuillez consulter les écrans du hall de la gare. <div className="aff-classique-quai-dep__fork-curve-rail" />
<div className="aff-classique-quai-dep__fork-branch-right">
<div className="aff-classique-quai-dep__fork-coaches-label">voitures 11 à 18</div>
<div className="aff-classique-quai-dep__fork-terminus-box">
<span className="aff-classique-quai-dep__bullet" style={{ position: 'static', margin: 0 }} />
<span className="aff-classique-quai-dep__fork-terminus-name">
{branch2Stops[branch2Stops.length - 1] || destinationName2}
</span>
</div>
</div>
</div>
{/* Branche principale continuant vers terminus 1 */}
{branch1Stops.length > 0 && (
<ul className="aff-classique-quai-dep__stops-list" style={{ marginTop: '8px' }}>
{branch1Stops.map((stopName, idx) => {
const isLast = idx === branch1Stops.length - 1;
return (
<li
key={idx}
className={`aff-classique-quai-dep__stop-item ${isLast ? 'aff-classique-quai-dep__stop-item--dest' : ''}`}
>
<span className="aff-classique-quai-dep__bullet" />
<span className="aff-classique-quai-dep__stop-name">{stopName}</span>
</li>
);
})}
</ul>
)}
</div> </div>
) : (
/* AFFICHAGE STANDARD DESSERTE VERTICALE (Conforme Image 1) */
stopsList.length > 0 ? (
<ul className="aff-classique-quai-dep__stops-list">
{stopsList.map((stopName, idx) => {
const isLast = idx === stopsList.length - 1;
return (
<li
key={idx}
className={`aff-classique-quai-dep__stop-item ${isLast ? 'aff-classique-quai-dep__stop-item--dest' : ''}`}
>
<span className="aff-classique-quai-dep__bullet" />
<span className="aff-classique-quai-dep__stop-name">
{stopName}
</span>
</li>
);
})}
</ul>
) : (
<div style={{ color: '#ffffff', fontSize: '38px', fontWeight: 600, padding: '40px' }}>
Veuillez consulter les écrans du hall de la gare.
</div>
)
)} )}
</div> </div>
</div> </div>
{/* BANDEAU BAS : PLEINE LARGEUR & HORLOGE (Conforme Image 1) */} {/* BANDEAU BAS : TGV COMPOSITION OU BANDEAU STANDARD */}
<div className="aff-classique-quai-dep__footer"> {isTgv ? (
<div className="aff-classique-quai-dep__footer-track"> <TgvCompositionQuai
{quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai ? `Voie ${cleanPlatform(horaire.quai).toUpperCase()}` : '')} platform={quaiParam || horaire?.quai || horaire?.quai_reel || horaire?.voie || horaire?._displayPlatform}
destination={destinationName1}
destination2={isDifferentDest ? destinationName2 : destinationName1}
composition={horaire?.composition}
marqueeMessage={horaire?._status?.causeDetails || horaire?.cause_retard || (isSuppressed ? `TRAIN SUPPRIMÉ - MERCI DE VOTRE COMPRÉHENSION` : (isDelayed ? `TRAIN RETARDÉ DE ${delayMinutes} MIN - MERCI DE VOTRE COMPRÉHENSION` : ''))}
/>
) : (
<div className="aff-classique-quai-dep__footer">
<div className="aff-classique-quai-dep__footer-track">
{quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai || horaire?.voie || horaire?._displayPlatform ? `Voie ${cleanPlatform(horaire.quai || horaire.voie || horaire._displayPlatform).toUpperCase()}` : '')}
</div>
<div className="aff-classique-quai-dep__footer-clock">
<DigitalClock />
</div>
</div> </div>
<div className="aff-classique-quai-dep__footer-clock"> )}
<DigitalClock />
</div>
</div>
</div> </div>
); );
} }
+90 -8
View File
@@ -24569,6 +24569,7 @@ function cleanStationQuery(s) {
const STATION_ALIASES = { const STATION_ALIASES = {
'valence tgv': '87763029', 'valence tgv': '87763029',
'valence': '87761007', 'valence': '87761007',
'valence ville': '87761007',
'paris nord': '87271007', 'paris nord': '87271007',
'paris gare du nord': '87271007', 'paris gare du nord': '87271007',
'paris est': '87113001', 'paris est': '87113001',
@@ -24581,11 +24582,18 @@ const STATION_ALIASES = {
'austerlitz': '87547000', 'austerlitz': '87547000',
'paris bercy': '87686667', 'paris bercy': '87686667',
'bercy': '87686667', 'bercy': '87686667',
'paris saint lazare': '87384008',
'saint lazare': '87384008',
'marseille': '87751008', 'marseille': '87751008',
'marseille saint charles': '87751008', 'marseille saint charles': '87751008',
'marseille st charles': '87751008', 'marseille st charles': '87751008',
'marseille blancarde': '87751081',
'lyon part dieu': '87723197', 'lyon part dieu': '87723197',
'lyon perrache': '87722025', 'lyon perrache': '87722025',
'lyon jean mace': '87282624',
'lyon gorge de loup': '87721175',
'lyon vaise': '87721001',
'lyon saint paul': '87721159',
'lyon saint exupery': '87762906', 'lyon saint exupery': '87762906',
'lyon st exupery': '87762906', 'lyon st exupery': '87762906',
'toulouse': '87611004', 'toulouse': '87611004',
@@ -24614,9 +24622,73 @@ const STATION_ALIASES = {
'besancon franche comte tgv': '87300863', 'besancon franche comte tgv': '87300863',
'dijon': '87713040', 'dijon': '87713040',
'dijon ville': '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' '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) { export function findGtfsGare(query) {
if (!query) return null; if (!query) return null;
const rawQ = String(query).trim(); const rawQ = String(query).trim();
@@ -24633,7 +24705,11 @@ export function findGtfsGare(query) {
if (found) return found; 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 qTokens = qClean.split(' ').filter(Boolean);
const wantsTgv = qTokens.includes('tgv'); const wantsTgv = qTokens.includes('tgv');
@@ -24649,8 +24725,12 @@ export function findGtfsGare(query) {
if (gClean === qClean) { if (gClean === qClean) {
score = 1000; score = 1000;
} else if (gClean.startsWith(qClean)) { } else if (matchStationNameStrict(g.nom, rawQ)) {
score = 500; 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 { } else {
let allFound = true; let allFound = true;
let tokenMatches = 0; let tokenMatches = 0;
@@ -24665,15 +24745,15 @@ export function findGtfsGare(query) {
} }
if (allFound) { if (allFound) {
score = 200 + tokenMatches * 20 - (gTokens.length - qTokens.length) * 5; score = 300 + tokenMatches * 20 - (gTokens.length - qTokens.length) * 40;
} else if (tokenMatches > 0) { } 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 += 150;
if (wantsTgv && !hasTgv) score -= 200; 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]) { if (!wantsTgv && gClean.includes('ville') && qTokens.length === 1 && gTokens[0] === qTokens[0]) {
score += 100; score += 100;
@@ -24707,6 +24787,8 @@ export function searchGtfsGares(query, limit = 20) {
if (gClean === qClean) { if (gClean === qClean) {
scored.push({ gare: g, score: 1000 }); scored.push({ gare: g, score: 1000 });
} else if (matchStationNameStrict(g.nom, query)) {
scored.push({ gare: g, score: 950 });
} else if (gClean.startsWith(qClean)) { } else if (gClean.startsWith(qClean)) {
scored.push({ gare: g, score: 800 }); scored.push({ gare: g, score: 800 });
} else { } else {
@@ -24718,9 +24800,9 @@ export function searchGtfsGares(query, limit = 20) {
} }
} }
if (matchCount === qTokens.length) { 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) { } else if (matchCount > 0) {
scored.push({ gare: g, score: matchCount * 20 - gTokens.length }); scored.push({ gare: g, score: matchCount * 20 - gTokens.length * 10 });
} }
} }
} }
+546 -85
View File
@@ -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_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_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_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 = { let gtfsRtCache = {
timestamp: 0, timestamp: 0,
updates: new Map(), updates: new Map(),
alerts: [] 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 = { let siriLiteCache = {
timestamp: 0, timestamp: 0,
platforms: new Map(), platforms: new Map(),
updates: 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 // 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 controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs); const id = setTimeout(() => controller.abort(), timeoutMs);
try { try {
@@ -40,12 +98,53 @@ function cleanNum(n) {
return m ? m[0].replace(/^0+/, '') : String(n).trim(); return m ? m[0].replace(/^0+/, '') : String(n).trim();
} }
function extractAllTrainNumbers(journeyXml) {
const nums = new Set();
// 1. TrainNumberRef
const tnr = journeyXml.match(/<TrainNumberRef>([^<]+)<\/TrainNumberRef>/);
if (tnr) {
const c = cleanNum(tnr[1]);
if (c) nums.add(c);
}
// 2. VehicleJourneyName
const vjn = journeyXml.match(/<VehicleJourneyName>([^<]+)<\/VehicleJourneyName>/);
if (vjn) {
const c = cleanNum(vjn[1]);
if (c) nums.add(c);
}
// 3. PublishedLineName if numeric
const pln = journeyXml.match(/<PublishedLineName>([^<]+)<\/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() { async function fetchSiriLiteData() {
const now = Date.now(); 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; return siriLiteCache;
} }
@@ -53,79 +152,112 @@ async function fetchSiriLiteData() {
const updatesMap = new Map(); const updatesMap = new Map();
try { 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) { if (res && res.ok) {
const xml = await res.text(); const xml = await res.text();
const journeys = xml.split('<EstimatedVehicleJourney>'); const journeys = xml.split('<EstimatedVehicleJourney>');
for (let i = 1; i < journeys.length; i++) { for (let i = 1; i < journeys.length; i++) {
const j = journeys[i]; const j = journeys[i];
const trainNumMatch = j.match(/<TrainNumberRef>([^<]+)<\/TrainNumberRef>/);
const rawTrainNum = trainNumMatch ? trainNumMatch[1].trim() : '';
const trainNum = cleanNum(rawTrainNum);
const journeyRefMatch = j.match(/<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/) || j.match(/<VehicleJourneyRef>([^<]+)<\/VehicleJourneyRef>/); // 1. Extraction exhaustive des identifiants et numéros de train
const trainNums = extractAllTrainNumbers(j);
const journeyRefMatch = j.match(/<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/) || j.match(/<VehicleJourneyRef>([^<]+)<\/VehicleJourneyRef>/) || j.match(/<FramedVehicleJourneyRef>[\s\S]*?<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/);
const journeyRef = journeyRefMatch ? journeyRefMatch[1].trim() : ''; const journeyRef = journeyRefMatch ? journeyRefMatch[1].trim() : '';
const isCanceled = j.includes('<Cancellation>true</Cancellation>'); const isCanceledJourney = j.includes('<Cancellation>true</Cancellation>') || j.includes('<JourneyStatus>cancelled</JourneyStatus>');
// Extraire les arrêts et quais (RecordedCall et EstimatedCall) // 2. Extraire tous les arrêts desservis (RecordedCall et EstimatedCall)
const callMatches = j.match(/<(RecordedCall|EstimatedCall)>[\s\S]*?<\/\1>/g) || []; const callMatches = j.match(/<(?:RecordedCall|EstimatedCall)>[\s\S]*?<\/(?:RecordedCall|EstimatedCall)>/g) || [];
for (const call of callMatches) { for (const call of callMatches) {
const stopRefMatch = call.match(/<StopPointRef>[^:]*::(\d+)<\/StopPointRef>/) || call.match(/<StopPointRef>([^<]+)<\/StopPointRef>/); // Extraction du code UIC de l'arrêt
const uicMatch = stopRefMatch ? (stopRefMatch[1].match(/\d{7,8}/) || [stopRefMatch[1]]) : null; const stopRefMatch = call.match(/<StopPointRef>([^<]+)<\/StopPointRef>/);
const uic = uicMatch ? uicMatch[0] : ''; 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>([^<]+)<\/StopPointName>/); const stopNameMatch = call.match(/<StopPointName>([^<]+)<\/StopPointName>/);
const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : ''; const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
const depPlatMatch = call.match(/<DeparturePlatformName>([^<]+)<\/DeparturePlatformName>/); // Extraction des quais (Départ, Arrivée, Prévu ou Réel)
const arrPlatMatch = call.match(/<ArrivalPlatformName>([^<]+)<\/ArrivalPlatformName>/); const depPlatMatch = call.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName)>([^<]+)<\/(?:DeparturePlatformName|PlannedDeparturePlatformName)>/);
const platform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : '')).trim(); const arrPlatMatch = call.match(/<(?:ArrivalPlatformName|PlannedArrivalPlatformName)>([^<]+)<\/(?:ArrivalPlatformName|PlannedArrivalPlatformName)>/);
const quayMatch = call.match(/<QuayRef>([^<]+)<\/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>([^<]+)<\/ExpectedDepartureTime>/); const expDepMatch = call.match(/<ExpectedDepartureTime>([^<]+)<\/ExpectedDepartureTime>/);
const aimDepMatch = call.match(/<AimedDepartureTime>([^<]+)<\/AimedDepartureTime>/); const aimDepMatch = call.match(/<AimedDepartureTime>([^<]+)<\/AimedDepartureTime>/);
const expArrMatch = call.match(/<ExpectedArrivalTime>([^<]+)<\/ExpectedArrivalTime>/);
const aimArrMatch = call.match(/<AimedArrivalTime>([^<]+)<\/AimedArrivalTime>/);
let delayMin = 0; let delayMin = 0;
if (expDepMatch && aimDepMatch) { const expTimeStr = (expDepMatch && expDepMatch[1]) || (expArrMatch && expArrMatch[1]);
const expD = new Date(expDepMatch[1]).getTime(); const aimTimeStr = (aimDepMatch && aimDepMatch[1]) || (aimArrMatch && aimArrMatch[1]);
const aimD = new Date(aimDepMatch[1]).getTime();
if (expTimeStr && aimTimeStr) {
const expD = new Date(expTimeStr).getTime();
const aimD = new Date(aimTimeStr).getTime();
if (!isNaN(expD) && !isNaN(aimD)) { if (!isNaN(expD) && !isNaN(aimD)) {
delayMin = Math.max(0, Math.round((expD - aimD) / 60000)); delayMin = Math.max(0, Math.round((expD - aimD) / 60000));
} }
} }
const isStopCancelled = isCanceledJourney || call.includes('<DepartureStatus>cancelled</DepartureStatus>') || call.includes('<ArrivalStatus>cancelled</ArrivalStatus>') || call.includes('<Cancellation>true</Cancellation>');
if (platform) { if (platform) {
if (uic) { for (const tNum of trainNums) {
if (trainNum) { if (uic) {
platformsMap.set(`${trainNum}_${uic}`, platform); platformsMap.set(`${tNum}_${uic}`, platform);
platformsMap.set(`${uic}_${trainNum}`, platform); platformsMap.set(`${uic}_${tNum}`, platform);
} }
if (rawTrainNum) { if (uic7) {
platformsMap.set(`${rawTrainNum}_${uic}`, platform); platformsMap.set(`${tNum}_${uic7}`, platform);
platformsMap.set(`${uic}_${rawTrainNum}`, 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(`${journeyRef}_${uic}`, platform);
platformsMap.set(`${uic}_${journeyRef}`, platform); platformsMap.set(`${uic}_${journeyRef}`, platform);
} }
} if (stopNameClean) {
if (stopNameClean) { platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
if (trainNum) platformsMap.set(`${trainNum}_${stopNameClean}`, platform); }
if (rawTrainNum) platformsMap.set(`${rawTrainNum}_${stopNameClean}`, platform);
if (journeyRef) platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
} }
} }
if (trainNum || rawTrainNum || journeyRef) { const info = {
const info = { platform: platform,
platform: platform, delayMinutes: delayMin,
delayMinutes: delayMin, isCanceled: isStopCancelled
isCanceled: isCanceled };
};
if (trainNum) updatesMap.set(trainNum, info); for (const tNum of trainNums) {
if (rawTrainNum) updatesMap.set(rawTrainNum, info); if (uic) updatesMap.set(`${tNum}_${uic}`, info);
if (journeyRef) updatesMap.set(journeyRef, 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 updates: updatesMap
}; };
} catch (err) { } 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; 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('<PtSituationElement>');
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>([^<]+)<\/TrainNumberRef>/) || s.match(/<VehicleJourneyRef>([^<]+)<\/VehicleJourneyRef>/) || s.match(/<PublishedLineName>([^<]+)<\/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>([^<]+)<\/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>([^<]+)<\/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(/<Summary[^>]*xml:lang="FR"[^>]*>([^<]+)<\/Summary>/i) || s.match(/<Summary[^>]*>([^<]+)<\/Summary>/i);
const summary = summaryFrMatch ? summaryFrMatch[1].trim() : '';
const descFrMatch = s.match(/<Description[^>]*xml:lang="FR"[^>]*>([^<]+)<\/Description>/i) || s.match(/<Description[^>]*>([^<]+)<\/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('<Cancellation>true</Cancellation>');
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 * 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 publicUrl = (typeof process !== 'undefined' && process.env && process.env.PUBLIC_URL) ? process.env.PUBLIC_URL : '';
const stationUrl = `${publicUrl}/data/stations/${uic}.json`; const stationUrl = `${publicUrl}/data/stations/${uic}.json`;
// Chargement parallèle tolérant aux pannes réseau / CORS // 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] = await Promise.all([ const [jsonRes, rtData, siriData, sxData, openDataPlatforms] = await Promise.all([
fetch(stationUrl).catch(err => { fetch(stationUrl).catch(err => {
console.warn(`[sncfApi] Notice: fetch station ${uic} error:`, err); console.warn(`[sncfApi] Notice: fetch station ${uic} error:`, err);
return null; return null;
@@ -322,8 +740,16 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
return { updates: new Map(), alerts: [] }; return { updates: new Map(), alerts: [] };
}), }),
fetchSiriLiteData().catch(err => { 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() }; 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 data = await jsonRes.json();
const gareRef = findGtfsGare(uic);
const targetStationNom = (gareRef && gareRef.nom) || (data && data.nom) || '';
const rawList = mode === 'arrivees' ? data.arrivees : data.departs; const rawList = mode === 'arrivees' ? data.arrivees : data.departs;
if (!Array.isArray(rawList) || rawList.length === 0) return null; if (!Array.isArray(rawList) || rawList.length === 0) return null;
@@ -362,21 +790,18 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
if (Array.isArray(customDaysRaw)) { if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw; customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') { } else if (typeof customDaysRaw === 'string') {
try { customDays = JSON.parse(customDaysRaw); } catch (e) {} try { customDays = JSON.parse(customDaysRaw); } catch (_) { customDays = []; }
}
if (Array.isArray(customDays) && customDays.length > 0) {
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetYYYYMMDD);
if (match) return Boolean(match.circule);
} }
if (customDays.includes(targetDateHyphen)) return true;
} }
// 3. Colonnes circule_lundi, circule_mardi... // 3. Base Supabase : circulation standard par jour de semaine
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; const daysMap = ['circule_dimanche', 'circule_lundi', 'circule_mardi', 'circule_mercredi', 'circule_jeudi', 'circule_vendredi', 'circule_samedi'];
const colName = `circule_${dayNames[targetDay]}`; const colName = daysMap[targetDay];
if (typeof t[colName] !== 'undefined' && t[colName] !== null) { if (typeof t[colName] !== 'undefined') {
const db = t.date_debut ? String(t.date_debut).slice(0, 10) : null; 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; 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; if (df && targetDateHyphen > df) return false;
return Boolean(t[colName]); return Boolean(t[colName]);
} }
@@ -399,21 +824,37 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
return true; return true;
}; };
// Helper de résolution de quai // Helper de résolution de quai en temps réel (Estimated Timetable + Situation Exchange + OpenData)
const lookupPlatform = (trainNum, tripId, stationUic, stationName) => { const lookupPlatform = (trainNum, tripId, stationUic, stationName, rawTimeStr = '', destName = '') => {
const cNum = cleanNum(trainNum); 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 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 ( return (
(cNum && stationUic && siriData.platforms.get(`${cNum}_${stationUic}`)) || // 1. SIRI-Lite Estimated Timetable (60 prochaines min - Priorité 1)
(trainNum && stationUic && siriData.platforms.get(`${trainNum}_${stationUic}`)) || (cNum && uicClean && siriData.platforms.get(`${cNum}_${uicClean}`)) ||
(stationUic && cNum && siriData.platforms.get(`${stationUic}_${cNum}`)) || (rawNum && uicClean && siriData.platforms.get(`${rawNum}_${uicClean}`)) ||
(stationUic && trainNum && siriData.platforms.get(`${stationUic}_${trainNum}`)) || (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}`)) || (cNum && cName && siriData.platforms.get(`${cNum}_${cName}`)) ||
(trainNum && cName && siriData.platforms.get(`${trainNum}_${cName}`)) || (rawNum && cName && siriData.platforms.get(`${rawNum}_${cName}`)) ||
(tripId && stationUic && siriData.platforms.get(`${tripId}_${stationUic}`)) || (tripId && uicClean && siriData.platforms.get(`${tripId}_${uicClean}`)) ||
(stationUic && tripId && siriData.platforms.get(`${stationUic}_${tripId}`)) ||
(tripId && cName && siriData.platforms.get(`${tripId}_${cName}`)) || (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 rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr);
const timeHM = parseTimeHM(rawTime) || 0; 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 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 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 delayMinutes = (rt && rt.delayMinutes > 0) ? rt.delayMinutes : (siri && siri.delayMinutes > 0 ? siri.delayMinutes : 0);
const currentStationName = mode === 'arrivees' ? t.dest : t.orig; const currentStationName = targetStationNom || (data && data.nom) || (mode === 'arrivees' ? t.dest : t.orig);
let realVoie = lookupPlatform(t.num, t.trip_id, uic, currentStationName); const destinationStation = mode === 'arrivees' ? t.orig : t.dest;
if (!realVoie && t.quai && typeof t.quai === 'string') { const isAutocar = (
realVoie = t.quai.trim(); (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"; let statusLabel = "à l'heure";
@@ -446,14 +900,24 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
etat = 'retarde'; etat = 'retarde';
} }
let cause = ''; let cause = (sx && sx.cause) || '';
if (!isTomorrow && delayMinutes > 0 && rtData.alerts.length > 0) { if (!isTomorrow && delayMinutes > 0 && !cause) {
cause = rtData.alerts[idx % rtData.alerts.length].text; 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 formattedStops = (t.stops || []).map((st) => {
const stopUic = st.uic || ''; 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 { return {
uic: stopUic, uic: stopUic,
@@ -580,13 +1044,10 @@ export async function getSncfTimetable({ gareName, mode = 'departs', quai = null
let finalTrains = null; let finalTrains = null;
if (uic) { if (uic) {
finalTrains = await fetchGtfsStationTimetable(uic, mode); finalTrains = await fetchGtfsStationTimetable(uic, mode);
} } else {
// Si UIC non trouvé directement, chercher par nom
// 2. Si non trouvé ou gare sans fichier, chercher avec le nom const match = findGtfsGare(gareName);
if (!finalTrains || finalTrains.length === 0) { if (match && match.uic) {
const rawGare = gare ? gare.nom : gareName;
const match = findGtfsGare(rawGare);
if (match && match.uic && match.uic !== uic) {
finalTrains = await fetchGtfsStationTimetable(match.uic, mode); finalTrains = await fetchGtfsStationTimetable(match.uic, mode);
} }
} }