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:
1 parent
9c6ac798f1
commit
d8f8ceaa9f
18 files changed
+2215
-424
No files matched your search
+40
-40
@@ -135,121 +135,121 @@
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-a.svg",
|
||||
"path": "/img/type/logo-rer-a.svg",
|
||||
"path": "/logo-rer-a.svg",
|
||||
"slug": "rer-a",
|
||||
"name": "RER A"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-b.svg",
|
||||
"path": "/img/type/logo-rer-b.svg",
|
||||
"path": "/logo-rer-b.svg",
|
||||
"slug": "rer-b",
|
||||
"name": "RER B"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-c.svg",
|
||||
"path": "/img/type/logo-rer-c.svg",
|
||||
"path": "/logo-rer-c.svg",
|
||||
"slug": "rer-c",
|
||||
"name": "RER C"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-d.svg",
|
||||
"path": "/img/type/logo-rer-d.svg",
|
||||
"path": "/logo-rer-d.svg",
|
||||
"slug": "rer-d",
|
||||
"name": "RER D"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-e.svg",
|
||||
"path": "/img/type/logo-rer-e.svg",
|
||||
"path": "/logo-rer-e.svg",
|
||||
"slug": "rer-e",
|
||||
"name": "RER E"
|
||||
},
|
||||
{
|
||||
"file": "logo-sbb.svg",
|
||||
"path": "/img/type/logo-sbb.svg",
|
||||
"path": "/logo-sbb.svg",
|
||||
"slug": "sbb",
|
||||
"name": "SBB"
|
||||
},
|
||||
{
|
||||
"file": "logo-sncb.svg",
|
||||
"path": "/img/type/logo-sncb.svg",
|
||||
"path": "/logo-sncb.svg",
|
||||
"slug": "sncb",
|
||||
"name": "SNCB"
|
||||
},
|
||||
{
|
||||
"file": "logo-sncf.svg",
|
||||
"path": "/img/type/logo-sncf.svg",
|
||||
"path": "/logo-sncf.svg",
|
||||
"slug": "sncf",
|
||||
"name": "Sncf"
|
||||
},
|
||||
{
|
||||
"file": "sncf-voyageurs-logo.png",
|
||||
"path": "/img/type/sncf-voyageurs-logo.png",
|
||||
"path": "/sncf-voyageurs-logo.png",
|
||||
"slug": "sncf-voyageurs-logo",
|
||||
"name": "SNCF"
|
||||
},
|
||||
{
|
||||
"file": "logo-sncf1985.svg",
|
||||
"path": "/img/type/logo-sncf1985.svg",
|
||||
"path": "/logo-sncf1985.svg",
|
||||
"slug": "sncf1985",
|
||||
"name": "Sncf1985"
|
||||
},
|
||||
{
|
||||
"file": "logo-sncf1992.svg",
|
||||
"path": "/img/type/logo-sncf1992.svg",
|
||||
"path": "/logo-sncf1992.svg",
|
||||
"slug": "sncf1992",
|
||||
"name": "Sncf1992"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-h.svg",
|
||||
"path": "/img/type/logo-t-h.svg",
|
||||
"path": "/logo-t-h.svg",
|
||||
"slug": "t-h",
|
||||
"name": "T H"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-j.svg",
|
||||
"path": "/img/type/logo-t-j.svg",
|
||||
"path": "/logo-t-j.svg",
|
||||
"slug": "t-j",
|
||||
"name": "T J"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-k.svg",
|
||||
"path": "/img/type/logo-t-k.svg",
|
||||
"path": "/logo-t-k.svg",
|
||||
"slug": "t-k",
|
||||
"name": "T K"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-l.svg",
|
||||
"path": "/img/type/logo-t-l.svg",
|
||||
"path": "/logo-t-l.svg",
|
||||
"slug": "t-l",
|
||||
"name": "T L"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-n.svg",
|
||||
"path": "/img/type/logo-t-n.svg",
|
||||
"path": "/logo-t-n.svg",
|
||||
"slug": "t-n",
|
||||
"name": "T N"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-p.svg",
|
||||
"path": "/img/type/logo-t-p.svg",
|
||||
"path": "/logo-t-p.svg",
|
||||
"slug": "t-p",
|
||||
"name": "T P"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-r.svg",
|
||||
"path": "/img/type/logo-t-r.svg",
|
||||
"path": "/logo-t-r.svg",
|
||||
"slug": "t-r",
|
||||
"name": "T R"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-u.svg",
|
||||
"path": "/img/type/logo-t-u.svg",
|
||||
"path": "/logo-t-u.svg",
|
||||
"slug": "t-u",
|
||||
"name": "T U"
|
||||
},
|
||||
{
|
||||
"file": "logo-teoz.svg",
|
||||
"path": "/img/type/logo-teoz.svg",
|
||||
"path": "/logo-teoz.svg",
|
||||
"slug": "teoz",
|
||||
"name": "Teoz"
|
||||
},
|
||||
@@ -261,121 +261,121 @@
|
||||
},
|
||||
{
|
||||
"file": "logo-ter-rhone-alpes.svg",
|
||||
"path": "/img/type/logo-ter-rhone-alpes.svg",
|
||||
"path": "/logo-ter-rhone-alpes.svg",
|
||||
"slug": "ter-rhone-alpes",
|
||||
"name": "TER Rhone-Alpes"
|
||||
},
|
||||
{
|
||||
"file": "logo-teralsace.svg",
|
||||
"path": "/img/type/logo-teralsace.svg",
|
||||
"path": "/logo-teralsace.svg",
|
||||
"slug": "teralsace",
|
||||
"name": "Ter Alsace"
|
||||
},
|
||||
{
|
||||
"file": "logo-teraquitaine.svg",
|
||||
"path": "/img/type/logo-teraquitaine.svg",
|
||||
"path": "/logo-teraquitaine.svg",
|
||||
"slug": "teraquitaine",
|
||||
"name": "Ter Aquitaine"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbassenormandie.svg",
|
||||
"path": "/img/type/logo-terbassenormandie.svg",
|
||||
"path": "/logo-terbassenormandie.svg",
|
||||
"slug": "terbassenormandie",
|
||||
"name": "Ter Basse Normandie"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbourgogne.svg",
|
||||
"path": "/img/type/logo-terbourgogne.svg",
|
||||
"path": "/logo-terbourgogne.svg",
|
||||
"slug": "terbourgogne",
|
||||
"name": "Ter Bourgogne"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbretagne.svg",
|
||||
"path": "/img/type/logo-terbretagne.svg",
|
||||
"path": "/logo-terbretagne.svg",
|
||||
"slug": "terbretagne",
|
||||
"name": "Ter Bretagne"
|
||||
},
|
||||
{
|
||||
"file": "logo-tercentre.svg",
|
||||
"path": "/img/type/logo-tercentre.svg",
|
||||
"path": "/logo-tercentre.svg",
|
||||
"slug": "tercentre",
|
||||
"name": "Ter Centre"
|
||||
},
|
||||
{
|
||||
"file": "logo-terlanguedocroussillon.svg",
|
||||
"path": "/img/type/logo-terlanguedocroussillon.svg",
|
||||
"path": "/logo-terlanguedocroussillon.svg",
|
||||
"slug": "terlanguedocroussillon",
|
||||
"name": "Ter Languedoc-Roussillon"
|
||||
},
|
||||
{
|
||||
"file": "logo-termidipyrenees.svg",
|
||||
"path": "/img/type/logo-termidipyrenees.svg",
|
||||
"path": "/logo-termidipyrenees.svg",
|
||||
"slug": "termidipyrenees",
|
||||
"name": "Ter Midi-Pyrenees"
|
||||
},
|
||||
{
|
||||
"file": "logo-ternordpasdecalais.svg",
|
||||
"path": "/img/type/logo-ternordpasdecalais.svg",
|
||||
"path": "/logo-ternordpasdecalais.svg",
|
||||
"slug": "ternordpasdecalais",
|
||||
"name": "Ter Nord-pas-de-Calais"
|
||||
},
|
||||
{
|
||||
"file": "logo-terpoitoucharentes.svg",
|
||||
"path": "/img/type/logo-terpoitoucharentes.svg",
|
||||
"path": "/logo-terpoitoucharentes.svg",
|
||||
"slug": "terpoitoucharentes",
|
||||
"name": "Ter Poitou Charentes"
|
||||
},
|
||||
{
|
||||
"file": "logo-tgv.svg",
|
||||
"path": "/img/type/logo-tgv.svg",
|
||||
"path": "/logo-tgv.svg",
|
||||
"slug": "tgv",
|
||||
"name": "TGV"
|
||||
},
|
||||
{
|
||||
"file": "logo-inoui.svg",
|
||||
"path": "/img/type/logo-inoui.svg",
|
||||
"path": "/logo-inoui.svg",
|
||||
"slug": "inoui",
|
||||
"name": "TGV InOui"
|
||||
},
|
||||
{
|
||||
"file": "logo-lyria.svg",
|
||||
"path": "/img/type/logo-lyria.svg",
|
||||
"path": "/logo-lyria.svg",
|
||||
"slug": "lyria",
|
||||
"name": "TGV Lyria"
|
||||
},
|
||||
{
|
||||
"file": "logo-thalys.svg",
|
||||
"path": "/img/type/logo-thalys.svg",
|
||||
"path": "/logo-thalys.svg",
|
||||
"slug": "thalys",
|
||||
"name": "Thalys"
|
||||
},
|
||||
{
|
||||
"file": "logo-thello.svg",
|
||||
"path": "/img/type/logo-thello.svg",
|
||||
"path": "/logo-thello.svg",
|
||||
"slug": "thello",
|
||||
"name": "Thello"
|
||||
},
|
||||
{
|
||||
"file": "track-car.svg",
|
||||
"path": "/img/type/track-car.svg",
|
||||
"path": "/track-car.svg",
|
||||
"slug": "track-car",
|
||||
"name": "Car TER"
|
||||
},
|
||||
{
|
||||
"file": "logo-tramtrain.svg",
|
||||
"path": "/img/type/logo-tramtrain.svg",
|
||||
"path": "/logo-tramtrain.svg",
|
||||
"slug": "tramtrain",
|
||||
"name": "Tramtrain"
|
||||
},
|
||||
{
|
||||
"file": "logo-trenitalia.svg",
|
||||
"path": "/img/type/logo-trenitalia.svg",
|
||||
"path": "/logo-trenitalia.svg",
|
||||
"slug": "trenitalia",
|
||||
"name": "Trenitalia"
|
||||
},
|
||||
{
|
||||
"file": "logo-zou.svg",
|
||||
"path": "/img/type/logo-zou.svg",
|
||||
"path": "/logo-zou.svg",
|
||||
"slug": "zou",
|
||||
"name": "Zou"
|
||||
}
|
||||
|
||||
@@ -3,379 +3,379 @@
|
||||
"logos": [
|
||||
{
|
||||
"file": "logo-aleop-color.svg",
|
||||
"path": "/type/quais/logo-aleop-color.svg",
|
||||
"path": "/logo-aleop-color.svg",
|
||||
"slug": "aleop",
|
||||
"name": "Aleop"
|
||||
},
|
||||
{
|
||||
"file": "logo-auvergerhonealpes-color.svg",
|
||||
"path": "/type/quais/logo-auvergerhonealpes-color.svg",
|
||||
"path": "/logo-auvergerhonealpes-color.svg",
|
||||
"slug": "auvergerhonealpes",
|
||||
"name": "Auvergerhonealpes"
|
||||
},
|
||||
{
|
||||
"file": "logo-breizhgo-color.svg",
|
||||
"path": "/type/quais/logo-breizhgo-color.svg",
|
||||
"path": "/logo-breizhgo-color.svg",
|
||||
"slug": "breizhgo",
|
||||
"name": "Breizhgo"
|
||||
},
|
||||
{
|
||||
"file": "logo-cfl-color.svg",
|
||||
"path": "/type/quais/logo-cfl-color.svg",
|
||||
"path": "/logo-cfl-color.svg",
|
||||
"slug": "cfl",
|
||||
"name": "CFL"
|
||||
},
|
||||
{
|
||||
"file": "logo-db-color.svg",
|
||||
"path": "/type/quais/logo-db-color.svg",
|
||||
"path": "/logo-db-color.svg",
|
||||
"slug": "db",
|
||||
"name": "DB"
|
||||
},
|
||||
{
|
||||
"file": "logo-eurostar-color.svg",
|
||||
"path": "/type/quais/logo-eurostar-color.svg",
|
||||
"path": "/logo-eurostar-color.svg",
|
||||
"slug": "eurostar",
|
||||
"name": "Eurostar"
|
||||
},
|
||||
{
|
||||
"file": "logo-fluo-color.svg",
|
||||
"path": "/type/quais/logo-fluo-color.svg",
|
||||
"path": "/logo-fluo-color.svg",
|
||||
"slug": "fluo",
|
||||
"name": "Fluo"
|
||||
},
|
||||
{
|
||||
"file": "logo-frecciarossa-color.svg",
|
||||
"path": "/type/quais/logo-frecciarossa-color.svg",
|
||||
"path": "/logo-frecciarossa-color.svg",
|
||||
"slug": "frecciarossa",
|
||||
"name": "Frecciarossa"
|
||||
},
|
||||
{
|
||||
"file": "logo-hautsdefrance-color.svg",
|
||||
"path": "/type/quais/logo-hautsdefrance-color.svg",
|
||||
"path": "/logo-hautsdefrance-color.svg",
|
||||
"slug": "hautsdefrance",
|
||||
"name": "Hautsdefrance"
|
||||
},
|
||||
{
|
||||
"file": "logo-ice-color.svg",
|
||||
"path": "/type/quais/logo-ice-color.svg",
|
||||
"path": "/logo-ice-color.svg",
|
||||
"slug": "ice",
|
||||
"name": "ICE"
|
||||
},
|
||||
{
|
||||
"file": "logo-intercites-color.svg",
|
||||
"path": "/type/quais/logo-intercites-color.svg",
|
||||
"path": "/logo-intercites-color.svg",
|
||||
"slug": "intercites",
|
||||
"name": "Intercités"
|
||||
},
|
||||
{
|
||||
"file": "logo-ler-color.svg",
|
||||
"path": "/type/quais/logo-ler-color.svg",
|
||||
"path": "/logo-ler-color.svg",
|
||||
"slug": "ler",
|
||||
"name": "Ler"
|
||||
},
|
||||
{
|
||||
"file": "logo-lio-color.svg",
|
||||
"path": "/type/quais/logo-lio-color.svg",
|
||||
"path": "/logo-lio-color.svg",
|
||||
"slug": "lio",
|
||||
"name": "Lio"
|
||||
},
|
||||
{
|
||||
"file": "logo-lunea-color.svg",
|
||||
"path": "/type/quais/logo-lunea-color.svg",
|
||||
"path": "/logo-lunea-color.svg",
|
||||
"slug": "lunea",
|
||||
"name": "Lunea"
|
||||
},
|
||||
{
|
||||
"file": "logo-metrolor-color.svg",
|
||||
"path": "/type/quais/logo-metrolor-color.svg",
|
||||
"path": "/logo-metrolor-color.svg",
|
||||
"slug": "metrolor",
|
||||
"name": "Metrolor"
|
||||
},
|
||||
{
|
||||
"file": "logo-mobigo-color.svg",
|
||||
"path": "/type/quais/logo-mobigo-color.svg",
|
||||
"path": "/logo-mobigo-color.svg",
|
||||
"slug": "mobigo",
|
||||
"name": "Mobigo"
|
||||
},
|
||||
{
|
||||
"file": "logo-nomad-color.svg",
|
||||
"path": "/type/quais/logo-nomad-color.svg",
|
||||
"path": "/logo-nomad-color.svg",
|
||||
"slug": "nomad",
|
||||
"name": "Nomad"
|
||||
},
|
||||
{
|
||||
"file": "logo-occitanie-color.svg",
|
||||
"path": "/type/quais/logo-occitanie-color.svg",
|
||||
"path": "/logo-occitanie-color.svg",
|
||||
"slug": "occitanie",
|
||||
"name": "Occitanie"
|
||||
},
|
||||
{
|
||||
"file": "logo-ouigo-color.svg",
|
||||
"path": "/type/quais/logo-ouigo-color.svg",
|
||||
"path": "/logo-ouigo-color.svg",
|
||||
"slug": "ouigo",
|
||||
"name": "OUIGO"
|
||||
},
|
||||
{
|
||||
"file": "logo-ouigo-classique-color.svg",
|
||||
"path": "/type/quais/logo-ouigo-classique-color.svg",
|
||||
"path": "/logo-ouigo-classique-color.svg",
|
||||
"slug": "ouigo-classique",
|
||||
"name": "OUIGO Trains Classiques"
|
||||
},
|
||||
{
|
||||
"file": "logo-remi-color.svg",
|
||||
"path": "/type/quais/logo-remi-color.svg",
|
||||
"path": "/logo-remi-color.svg",
|
||||
"slug": "remi",
|
||||
"name": "Remi"
|
||||
},
|
||||
{
|
||||
"file": "logo-renfeave-color.svg",
|
||||
"path": "/type/quais/logo-renfeave-color.svg",
|
||||
"path": "/logo-renfeave-color.svg",
|
||||
"slug": "renfeave",
|
||||
"name": "Renfe AVE"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-a-color.svg",
|
||||
"path": "/type/quais/logo-rer-a-color.svg",
|
||||
"path": "/logo-rer-a-color.svg",
|
||||
"slug": "rer-a",
|
||||
"name": "RER A"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-b-color.svg",
|
||||
"path": "/type/quais/logo-rer-b-color.svg",
|
||||
"path": "/logo-rer-b-color.svg",
|
||||
"slug": "rer-b",
|
||||
"name": "RER B"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-c-color.svg",
|
||||
"path": "/type/quais/logo-rer-c-color.svg",
|
||||
"path": "/logo-rer-c-color.svg",
|
||||
"slug": "rer-c",
|
||||
"name": "RER C"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-d-color.svg",
|
||||
"path": "/type/quais/logo-rer-d-color.svg",
|
||||
"path": "/logo-rer-d-color.svg",
|
||||
"slug": "rer-d",
|
||||
"name": "RER D"
|
||||
},
|
||||
{
|
||||
"file": "logo-rer-e-color.svg",
|
||||
"path": "/type/quais/logo-rer-e-color.svg",
|
||||
"path": "/logo-rer-e-color.svg",
|
||||
"slug": "rer-e",
|
||||
"name": "RER E"
|
||||
},
|
||||
{
|
||||
"file": "logo-sbb-color.svg",
|
||||
"path": "/type/quais/logo-sbb-color.svg",
|
||||
"path": "/logo-sbb-color.svg",
|
||||
"slug": "sbb",
|
||||
"name": "SBB"
|
||||
},
|
||||
{
|
||||
"file": "logo-sncb-color.svg",
|
||||
"path": "/type/quais/logo-sncb-color.svg",
|
||||
"path": "/logo-sncb-color.svg",
|
||||
"slug": "sncb",
|
||||
"name": "SNCB"
|
||||
},
|
||||
{
|
||||
"file": "logo-sncf-color.svg",
|
||||
"path": "/type/quais/logo-sncf-color.svg",
|
||||
"path": "/logo-sncf-color.svg",
|
||||
"slug": "sncf",
|
||||
"name": "Sncf"
|
||||
},
|
||||
{
|
||||
"file": "sncf-voyageurs-logo-color.png",
|
||||
"path": "/type/quais/img/type/sncf-voyageurs-logo-color.png",
|
||||
"path": "/img/type/sncf-voyageurs-logo-color.png",
|
||||
"slug": "sncf-voyageurs-logo",
|
||||
"name": "SNCF"
|
||||
},
|
||||
{
|
||||
"file": "logo-sncf1985.svg",
|
||||
"path": "/type/quais/img/type/logo-sncf1985.svg",
|
||||
"path": "/img/type/logo-sncf1985.svg",
|
||||
"slug": "sncf1985",
|
||||
"name": "Sncf1985"
|
||||
},
|
||||
{
|
||||
"file": "logo-sncf1992.svg",
|
||||
"path": "/type/quais/img/type/logo-sncf1992.svg",
|
||||
"path": "/img/type/logo-sncf1992.svg",
|
||||
"slug": "sncf1992",
|
||||
"name": "Sncf1992"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-h.svg",
|
||||
"path": "/type/quais/img/type/logo-t-h.svg",
|
||||
"path": "/img/type/logo-t-h.svg",
|
||||
"slug": "t-h",
|
||||
"name": "T H"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-j.svg",
|
||||
"path": "/type/quais/img/type/logo-t-j.svg",
|
||||
"path": "/img/type/logo-t-j.svg",
|
||||
"slug": "t-j",
|
||||
"name": "T J"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-k.svg",
|
||||
"path": "/type/quais/img/type/logo-t-k.svg",
|
||||
"path": "/img/type/logo-t-k.svg",
|
||||
"slug": "t-k",
|
||||
"name": "T K"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-l.svg",
|
||||
"path": "/type/quais/img/type/logo-t-l.svg",
|
||||
"path": "/img/type/logo-t-l.svg",
|
||||
"slug": "t-l",
|
||||
"name": "T L"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-n.svg",
|
||||
"path": "/type/quais/img/type/logo-t-n.svg",
|
||||
"path": "/img/type/logo-t-n.svg",
|
||||
"slug": "t-n",
|
||||
"name": "T N"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-p.svg",
|
||||
"path": "/type/quais/img/type/logo-t-p.svg",
|
||||
"path": "/img/type/logo-t-p.svg",
|
||||
"slug": "t-p",
|
||||
"name": "T P"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-r.svg",
|
||||
"path": "/type/quais/img/type/logo-t-r.svg",
|
||||
"path": "/img/type/logo-t-r.svg",
|
||||
"slug": "t-r",
|
||||
"name": "T R"
|
||||
},
|
||||
{
|
||||
"file": "logo-t-u.svg",
|
||||
"path": "/type/quais/img/type/logo-t-u.svg",
|
||||
"path": "/img/type/logo-t-u.svg",
|
||||
"slug": "t-u",
|
||||
"name": "T U"
|
||||
},
|
||||
{
|
||||
"file": "logo-teoz.svg",
|
||||
"path": "/type/quais/img/type/logo-teoz.svg",
|
||||
"path": "/img/type/logo-teoz.svg",
|
||||
"slug": "teoz",
|
||||
"name": "Teoz"
|
||||
},
|
||||
{
|
||||
"file": "sncf-voyageurs-logo.png",
|
||||
"path": "/type/quais/sncf-voyageurs-logo.png",
|
||||
"path": "/sncf-voyageurs-logo.png",
|
||||
"slug": "ter",
|
||||
"name": "TER"
|
||||
},
|
||||
{
|
||||
"file": "logo-ter-rhone-alpes.svg",
|
||||
"path": "/type/quais/logo-ter-rhone-alpes.svg",
|
||||
"path": "/logo-ter-rhone-alpes.svg",
|
||||
"slug": "ter-rhone-alpes",
|
||||
"name": "TER Rhone Alpes"
|
||||
},
|
||||
{
|
||||
"file": "logo-teralsace.svg",
|
||||
"path": "/type/quais/logo-teralsace.svg",
|
||||
"path": "/logo-teralsace.svg",
|
||||
"slug": "teralsace",
|
||||
"name": "Teralsace"
|
||||
},
|
||||
{
|
||||
"file": "logo-teraquitaine.svg",
|
||||
"path": "/type/quais/logo-teraquitaine.svg",
|
||||
"path": "/logo-teraquitaine.svg",
|
||||
"slug": "teraquitaine",
|
||||
"name": "Teraquitaine"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbassenormandie.svg",
|
||||
"path": "/type/quais/logo-terbassenormandie.svg",
|
||||
"path": "/logo-terbassenormandie.svg",
|
||||
"slug": "terbassenormandie",
|
||||
"name": "Terbassenormandie"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbourgogne.svg",
|
||||
"path": "/type/quais/logo-terbourgogne.svg",
|
||||
"path": "/logo-terbourgogne.svg",
|
||||
"slug": "terbourgogne",
|
||||
"name": "Terbourgogne"
|
||||
},
|
||||
{
|
||||
"file": "logo-terbretagne.svg",
|
||||
"path": "/type/quais/logo-terbretagne.svg",
|
||||
"path": "/logo-terbretagne.svg",
|
||||
"slug": "terbretagne",
|
||||
"name": "Terbretagne"
|
||||
},
|
||||
{
|
||||
"file": "logo-tercentre.svg",
|
||||
"path": "/type/quais/logo-tercentre.svg",
|
||||
"path": "/logo-tercentre.svg",
|
||||
"slug": "tercentre",
|
||||
"name": "Tercentre"
|
||||
},
|
||||
{
|
||||
"file": "logo-terlanguedocroussillon.svg",
|
||||
"path": "/type/quais/logo-terlanguedocroussillon.svg",
|
||||
"path": "/logo-terlanguedocroussillon.svg",
|
||||
"slug": "terlanguedocroussillon",
|
||||
"name": "Terlanguedocroussillon"
|
||||
},
|
||||
{
|
||||
"file": "logo-termidipyrenees.svg",
|
||||
"path": "/type/quais/logo-termidipyrenees.svg",
|
||||
"path": "/logo-termidipyrenees.svg",
|
||||
"slug": "termidipyrenees",
|
||||
"name": "Termidipyrenees"
|
||||
},
|
||||
{
|
||||
"file": "logo-ternordpasdecalais.svg",
|
||||
"path": "/type/quais/logo-ternordpasdecalais.svg",
|
||||
"path": "/logo-ternordpasdecalais.svg",
|
||||
"slug": "ternordpasdecalais",
|
||||
"name": "Ternordpasdecalais"
|
||||
},
|
||||
{
|
||||
"file": "logo-terpoitoucharentes.svg",
|
||||
"path": "/type/quais/logo-terpoitoucharentes.svg",
|
||||
"path": "/logo-terpoitoucharentes.svg",
|
||||
"slug": "terpoitoucharentes",
|
||||
"name": "Terpoitoucharentes"
|
||||
},
|
||||
{
|
||||
"file": "logo-tgv.svg",
|
||||
"path": "/type/quais/logo-tgv.svg",
|
||||
"path": "/logo-tgv.svg",
|
||||
"slug": "tgv",
|
||||
"name": "TGV"
|
||||
},
|
||||
{
|
||||
"file": "logo-inoui.svg",
|
||||
"path": "/type/quais/logo-inoui.svg",
|
||||
"path": "/logo-inoui.svg",
|
||||
"slug": "inoui",
|
||||
"name": "TGV InOui"
|
||||
},
|
||||
{
|
||||
"file": "logo-lyria.svg",
|
||||
"path": "/type/quais/logo-lyria.svg",
|
||||
"path": "/logo-lyria.svg",
|
||||
"slug": "lyria",
|
||||
"name": "TGV Lyria"
|
||||
},
|
||||
{
|
||||
"file": "logo-thalys.svg",
|
||||
"path": "/type/quais/logo-thalys.svg",
|
||||
"path": "/logo-thalys.svg",
|
||||
"slug": "thalys",
|
||||
"name": "Thalys"
|
||||
},
|
||||
{
|
||||
"file": "logo-thello.svg",
|
||||
"path": "/type/quais/logo-thello.svg",
|
||||
"path": "/logo-thello.svg",
|
||||
"slug": "thello",
|
||||
"name": "Thello"
|
||||
},
|
||||
{
|
||||
"file": "track-car.svg",
|
||||
"path": "/type/quais/track-car.svg",
|
||||
"path": "/track-car.svg",
|
||||
"slug": "track-car",
|
||||
"name": "Track Car"
|
||||
},
|
||||
{
|
||||
"file": "logo-tramtrain.svg",
|
||||
"path": "/type/quais/logo-tramtrain.svg",
|
||||
"path": "/logo-tramtrain.svg",
|
||||
"slug": "tramtrain",
|
||||
"name": "Tramtrain"
|
||||
},
|
||||
{
|
||||
"file": "logo-trenitalia.svg",
|
||||
"path": "/type/quais/logo-trenitalia.svg",
|
||||
"path": "/logo-trenitalia.svg",
|
||||
"slug": "trenitalia",
|
||||
"name": "Trenitalia"
|
||||
},
|
||||
{
|
||||
"file": "logo-zou.svg",
|
||||
"path": "/type/quais/logo-zou.svg",
|
||||
"path": "/logo-zou.svg",
|
||||
"slug": "zou",
|
||||
"name": "Zou"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import MarqueeText from '../../../components/MarqueeText';
|
||||
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
||||
import { getSourceFromPath } from '../../../services/railwayService';
|
||||
import { getSncfTimetable } from '../../../services/sncfApi';
|
||||
import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi';
|
||||
import { matchStationNameStrict } from '../../../services/gtfsStations';
|
||||
import AFLSecurityPage from './components/AFLSecurityPage';
|
||||
import AFLCorrespondancesPage from './components/AFLCorrespondancesPage';
|
||||
import AFLInformationsPage from './components/AFLInformationsPage';
|
||||
@@ -219,12 +220,8 @@ export default function AFLArrivees(){
|
||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
||||
if (!gerr && gdata && gdata.length) {
|
||||
const exactMatch = gdata.find(g => {
|
||||
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
return n1 === n2;
|
||||
});
|
||||
const chosen = exactMatch || gdata[0];
|
||||
const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
|
||||
const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
|
||||
fetchedType = chosen ? chosen.type : null;
|
||||
fetchedId = chosen ? chosen.id : null;
|
||||
}
|
||||
@@ -662,17 +659,18 @@ export default function AFLArrivees(){
|
||||
const stopForStation = (stops || []).find(s => {
|
||||
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
|
||||
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
|
||||
const ns = normalizeName(nm || '');
|
||||
const target = normalizeName(targetStationName || '');
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
|
||||
}) || null;
|
||||
horaire._status = displayStatus;
|
||||
horaire._displayTime = displayTime;
|
||||
const platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || '';
|
||||
horaire._displayPlatform = String(platformVal).trim();
|
||||
let platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform || stopForStation.voie)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || horaire.voie || '';
|
||||
if (!platformVal) {
|
||||
platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
|
||||
}
|
||||
horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return horaire;
|
||||
});
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
|
||||
|
||||
// Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu
|
||||
useEffect(() => {
|
||||
@@ -699,11 +697,13 @@ export default function AFLArrivees(){
|
||||
} else {
|
||||
const formatted = (sncfData || []).map(t => {
|
||||
const timeStr = t.heure_arrivee || t.heure_depart || t.time || '';
|
||||
const arrHM = (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr);
|
||||
const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return {
|
||||
...t,
|
||||
_displayTime: formatTimeNoSeconds(timeStr),
|
||||
_displayPlatform: t.quai || t.voie || '',
|
||||
_arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr),
|
||||
_displayPlatform: plat,
|
||||
_arrHM: arrHM,
|
||||
_status: t._status || {
|
||||
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
|
||||
detail: '',
|
||||
@@ -784,25 +784,24 @@ export default function AFLArrivees(){
|
||||
// comme une gare d'arrivée (soit le terminus, soit un arrêt intermédiaire)
|
||||
let displayHoraires = parsedHoraires.slice();
|
||||
if (gareDisplay) {
|
||||
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
|
||||
displayHoraires = displayHoraires.filter(h => {
|
||||
// Exclure si la gare ciblée est la gare de départ (on veut les arrivées, pas les départs)
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString());
|
||||
if (departName === normalized) return false;
|
||||
const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
|
||||
if (matchStationNameStrict(departName, gareDisplay)) return false;
|
||||
|
||||
// Inclure si gare d'arrivée finale correspond
|
||||
const arriveeName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (arriveeName === normalized) return true;
|
||||
const arriveeName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
|
||||
if (matchStationNameStrict(arriveeName, gareDisplay)) return true;
|
||||
|
||||
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est l'origine (isFirst ou index==0)
|
||||
const stopsList = getStops(h);
|
||||
if (Array.isArray(stopsList) && stopsList.length) {
|
||||
for (const s of stopsList) {
|
||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
|
||||
const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString();
|
||||
if (!stopName) continue;
|
||||
const isOrigin = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && Number(s.index) === 0));
|
||||
if (isOrigin) continue; // Si c'est l'origine, ça ne peut pas être une arrivée
|
||||
if (stopName === normalized) return true;
|
||||
if (matchStationNameStrict(stopName, gareDisplay)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -868,7 +867,7 @@ export default function AFLArrivees(){
|
||||
const stopsListForH = getStops(h);
|
||||
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
|
||||
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null;
|
||||
const stop = byId || byName;
|
||||
if (stop && (stop.heure_arrivee || stop.time || stop.heure)) raw = stop.heure_arrivee || stop.time || stop.heure;
|
||||
}
|
||||
@@ -1300,7 +1299,17 @@ export default function AFLArrivees(){
|
||||
).toString().trim();
|
||||
|
||||
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
||||
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
|
||||
const isCarTER = (
|
||||
op === 'CAR TER' ||
|
||||
op === 'AUTOCAR' ||
|
||||
op.includes('AUTOCAR') ||
|
||||
op.includes('CAR') ||
|
||||
op.includes('BUS') ||
|
||||
rawPlatform === 'AUTOCAR' ||
|
||||
rawPlatform === 'CAR' ||
|
||||
rawPlatform === 'BUS' ||
|
||||
isSub
|
||||
);
|
||||
|
||||
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure || '');
|
||||
const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue';
|
||||
|
||||
@@ -6,6 +6,7 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||
import Logo from '../../../components/Logo';
|
||||
import { getSourceFromPath } from '../../../services/railwayService';
|
||||
import { getSncfTimetable } from '../../../services/sncfApi';
|
||||
import { matchStationNameStrict } from '../../../services/gtfsStations';
|
||||
import GareVideoprotection from '../../../components/afficheurs/GareVideoprotection';
|
||||
import AttentifsEnsemble from '../../../components/afficheurs/AttentifsEnsemble';
|
||||
import AFLInformationsPage from './components/AFLInformationsPage';
|
||||
@@ -373,9 +374,20 @@ const AFLArriveesQuais = () => {
|
||||
quai: quaiParam
|
||||
});
|
||||
if (sncfData && sncfData.length > 0) {
|
||||
const train = sncfData[0];
|
||||
let matching = sncfData;
|
||||
if (quaiParam) {
|
||||
const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
|
||||
matching = sncfData.filter(t => cleanP(t._displayPlatform || t.quai || t.voie) === cleanP(quaiParam));
|
||||
}
|
||||
const train = matching[0];
|
||||
if (train) {
|
||||
const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || '';
|
||||
const enriched = {
|
||||
...train,
|
||||
quai: trainPlatform,
|
||||
quai_reel: trainPlatform,
|
||||
voie: trainPlatform,
|
||||
_displayPlatform: trainPlatform,
|
||||
_rawTime: train.heure_arrivee || train.heure_depart,
|
||||
_displayTime: formatTimeNoSeconds(train.heure_arrivee || train.heure_depart),
|
||||
_status: {
|
||||
@@ -392,6 +404,9 @@ const AFLArriveesQuais = () => {
|
||||
} else {
|
||||
setNextArrival(null);
|
||||
}
|
||||
} else {
|
||||
setNextArrival(null);
|
||||
}
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -591,25 +606,24 @@ const AFLArriveesQuais = () => {
|
||||
// Filter by station: keep arrivals to this station or stops at this station (excluding origin)
|
||||
let stationFiltered = enriched;
|
||||
if (gareDisplay) {
|
||||
const normalized = normalizeName(gareDisplay);
|
||||
stationFiltered = enriched.filter(h => {
|
||||
// Exclude if target station is the departure origin
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString());
|
||||
if (departName === normalized) return false;
|
||||
const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
|
||||
if (matchStationNameStrict(departName, gareDisplay)) return false;
|
||||
|
||||
// Include if target station is the terminus
|
||||
const arriveeName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (arriveeName === normalized) return true;
|
||||
const arriveeName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
|
||||
if (matchStationNameStrict(arriveeName, gareDisplay)) return true;
|
||||
|
||||
// Check intermediate stops
|
||||
const stopsList = getStops(h);
|
||||
if (Array.isArray(stopsList) && stopsList.length) {
|
||||
for (const s of stopsList) {
|
||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || '').toString());
|
||||
const stopName = (s.gare_nom || s.nom || s.name || '').toString();
|
||||
if (!stopName) continue;
|
||||
const isOrigin = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && Number(s.index) === 0));
|
||||
if (isOrigin) continue;
|
||||
if (stopName === normalized) return true;
|
||||
if (matchStationNameStrict(stopName, gareDisplay)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -676,7 +690,7 @@ const AFLArriveesQuais = () => {
|
||||
if (gareDisplay && Array.isArray(stopsList)) {
|
||||
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
|
||||
}
|
||||
const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
|
||||
const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
|
||||
if (!quaiParam) return true;
|
||||
const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
|
||||
return cleanP(platform) === cleanP(quaiParam);
|
||||
@@ -862,10 +876,9 @@ const AFLArriveesQuais = () => {
|
||||
|
||||
let journeyStops = [];
|
||||
if (Array.isArray(stopsList) && stopsList.length > 0) {
|
||||
const normCurrent = normalizeName(currentStationName);
|
||||
const endIdx = stopsList.findIndex(s => {
|
||||
const sName = normalizeName((s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString());
|
||||
return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName));
|
||||
const sName = (s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString();
|
||||
return sName && matchStationNameStrict(sName, currentStationName);
|
||||
});
|
||||
|
||||
const rawStops = endIdx !== -1 ? stopsList.slice(0, endIdx + 1) : stopsList.slice();
|
||||
|
||||
@@ -6,7 +6,8 @@ import MarqueeText from '../../../components/MarqueeText';
|
||||
import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
||||
import { getSourceFromPath } from '../../../services/railwayService';
|
||||
import { getSncfTimetable } from '../../../services/sncfApi';
|
||||
import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi';
|
||||
import { matchStationNameStrict } from '../../../services/gtfsStations';
|
||||
import AFLSecurityPage from './components/AFLSecurityPage';
|
||||
import AFLCorrespondancesPage from './components/AFLCorrespondancesPage';
|
||||
import AFLInformationsPage from './components/AFLInformationsPage';
|
||||
@@ -227,12 +228,8 @@ export default function AFLDeparts(){
|
||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
||||
if (!gerr && gdata && gdata.length) {
|
||||
const exactMatch = gdata.find(g => {
|
||||
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
return n1 === n2;
|
||||
});
|
||||
const chosen = exactMatch || gdata[0];
|
||||
const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
|
||||
const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
|
||||
fetchedType = chosen ? chosen.type : null;
|
||||
fetchedId = chosen ? chosen.id : null;
|
||||
}
|
||||
@@ -670,17 +667,18 @@ export default function AFLDeparts(){
|
||||
const stopForStation = (stops || []).find(s => {
|
||||
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
|
||||
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
|
||||
const ns = normalizeName(nm || '');
|
||||
const target = normalizeName(targetStationName || '');
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
|
||||
}) || null;
|
||||
horaire._status = displayStatus;
|
||||
horaire._displayTime = displayTime;
|
||||
const platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || '';
|
||||
horaire._displayPlatform = String(platformVal).trim();
|
||||
let platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform || stopForStation.voie)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || horaire.voie || '';
|
||||
if (!platformVal) {
|
||||
platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
|
||||
}
|
||||
horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return horaire;
|
||||
});
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setShowStatus(s => !s), 3000);
|
||||
@@ -710,18 +708,22 @@ export default function AFLDeparts(){
|
||||
if (sncfErr) {
|
||||
setError(sncfErr);
|
||||
} else {
|
||||
const formatted = (sncfData || []).map(t => ({
|
||||
const formatted = (sncfData || []).map(t => {
|
||||
const depHM = (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart);
|
||||
const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return {
|
||||
...t,
|
||||
_displayTime: formatTimeNoSeconds(t.heure_depart),
|
||||
_displayPlatform: t.quai || t.voie || '',
|
||||
_depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart),
|
||||
_displayPlatform: plat,
|
||||
_depHM: depHM,
|
||||
_status: t._status || {
|
||||
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
|
||||
detail: '',
|
||||
delayMinutes: t.retard_minutes || null,
|
||||
causeDetails: t.cause_retard || null
|
||||
}
|
||||
}));
|
||||
};
|
||||
});
|
||||
setHoraires(formatted);
|
||||
setAllHoraires(formatted);
|
||||
}
|
||||
@@ -795,15 +797,14 @@ export default function AFLDeparts(){
|
||||
// Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
|
||||
let displayHoraires = parsedHoraires.slice();
|
||||
if (gareDisplay) {
|
||||
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
|
||||
displayHoraires = displayHoraires.filter(h => {
|
||||
// Exclure si la gare ciblée est la gare d'arrivée finale
|
||||
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (destName === normalized) return false;
|
||||
const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
|
||||
if (matchStationNameStrict(destName, gareDisplay)) return false;
|
||||
|
||||
// Inclure si gare de départ correspond
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString());
|
||||
if (departName === normalized) return true;
|
||||
const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString();
|
||||
if (matchStationNameStrict(departName, gareDisplay)) return true;
|
||||
|
||||
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max)
|
||||
const stopsList = getStops(h);
|
||||
@@ -816,11 +817,11 @@ export default function AFLDeparts(){
|
||||
}
|
||||
}
|
||||
for (const s of stopsList) {
|
||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
|
||||
const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString();
|
||||
if (!stopName) continue;
|
||||
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
|
||||
if (isTerminus) continue;
|
||||
if (stopName === normalized) return true;
|
||||
if (matchStationNameStrict(stopName, gareDisplay)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -893,7 +894,7 @@ export default function AFLDeparts(){
|
||||
const stopsListForH = getStops(h);
|
||||
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
|
||||
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null;
|
||||
const stop = byId || byName;
|
||||
if (stop && (stop.heure_depart || stop.time || stop.heure)) raw = stop.heure_depart || stop.time || stop.heure;
|
||||
}
|
||||
@@ -1325,7 +1326,17 @@ export default function AFLDeparts(){
|
||||
).toString().trim();
|
||||
|
||||
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
||||
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
|
||||
const isCarTER = (
|
||||
op === 'CAR TER' ||
|
||||
op === 'AUTOCAR' ||
|
||||
op.includes('AUTOCAR') ||
|
||||
op.includes('CAR') ||
|
||||
op.includes('BUS') ||
|
||||
rawPlatform === 'AUTOCAR' ||
|
||||
rawPlatform === 'CAR' ||
|
||||
rawPlatform === 'BUS' ||
|
||||
isSub
|
||||
);
|
||||
|
||||
let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || '');
|
||||
const destination = horaire.gare_arrivee_nom || '';
|
||||
|
||||
@@ -6,6 +6,7 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||
import Logo from '../../../components/Logo';
|
||||
import { getSourceFromPath } from '../../../services/railwayService';
|
||||
import { getSncfTimetable } from '../../../services/sncfApi';
|
||||
import { matchStationNameStrict } from '../../../services/gtfsStations';
|
||||
import GareVideoprotection from '../../../components/afficheurs/GareVideoprotection';
|
||||
import AttentifsEnsemble from '../../../components/afficheurs/AttentifsEnsemble';
|
||||
import DepartEnCoursAFL from '../../../components/afficheurs/DepartEnCoursAFL';
|
||||
@@ -445,9 +446,20 @@ const AFLDepartsQuais = () => {
|
||||
quai: quaiParam
|
||||
});
|
||||
if (sncfData && sncfData.length > 0) {
|
||||
const train = sncfData[0];
|
||||
let matching = sncfData;
|
||||
if (quaiParam) {
|
||||
const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
|
||||
matching = sncfData.filter(t => cleanP(t._displayPlatform || t.quai || t.voie) === cleanP(quaiParam));
|
||||
}
|
||||
const train = matching[0];
|
||||
if (train) {
|
||||
const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || '';
|
||||
const enriched = {
|
||||
...train,
|
||||
quai: trainPlatform,
|
||||
quai_reel: trainPlatform,
|
||||
voie: trainPlatform,
|
||||
_displayPlatform: trainPlatform,
|
||||
_rawTime: train.heure_depart,
|
||||
_displayTime: formatTimeNoSeconds(train.heure_depart),
|
||||
_status: {
|
||||
@@ -464,6 +476,9 @@ const AFLDepartsQuais = () => {
|
||||
} else {
|
||||
setNextDeparture(null);
|
||||
}
|
||||
} else {
|
||||
setNextDeparture(null);
|
||||
}
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -663,13 +678,12 @@ const AFLDepartsQuais = () => {
|
||||
// Filter by station: keep departures from this station or stops at this station (excluding terminus)
|
||||
let stationFiltered = enriched;
|
||||
if (gareDisplay) {
|
||||
const normalized = normalizeName(gareDisplay);
|
||||
stationFiltered = enriched.filter(h => {
|
||||
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (destName === normalized) return false;
|
||||
const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
|
||||
if (matchStationNameStrict(destName, gareDisplay)) return false;
|
||||
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString());
|
||||
if (departName === normalized) return true;
|
||||
const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
|
||||
if (matchStationNameStrict(departName, gareDisplay)) return true;
|
||||
|
||||
const stopsList = getStops(h);
|
||||
if (Array.isArray(stopsList) && stopsList.length) {
|
||||
@@ -681,11 +695,11 @@ const AFLDepartsQuais = () => {
|
||||
}
|
||||
}
|
||||
for (const s of stopsList) {
|
||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || '').toString());
|
||||
const stopName = (s.gare_nom || s.nom || s.name || '').toString();
|
||||
if (!stopName) continue;
|
||||
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
|
||||
if (isTerminus) continue;
|
||||
if (stopName === normalized) return true;
|
||||
if (matchStationNameStrict(stopName, gareDisplay)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -751,7 +765,7 @@ const AFLDepartsQuais = () => {
|
||||
if (gareDisplay && Array.isArray(stopsList)) {
|
||||
stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay));
|
||||
}
|
||||
const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
|
||||
const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || h.quai_reel || h.voie || h.quai || '').toString().trim();
|
||||
if (!quaiParam) return true;
|
||||
const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
|
||||
return cleanP(platform) === cleanP(quaiParam);
|
||||
@@ -964,10 +978,9 @@ const AFLDepartsQuais = () => {
|
||||
|
||||
let downstreamStops = [];
|
||||
if (Array.isArray(stopsList) && stopsList.length > 0) {
|
||||
const normCurrent = normalizeName(currentStationName);
|
||||
const startIdx = stopsList.findIndex(s => {
|
||||
const sName = normalizeName((s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString());
|
||||
return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName));
|
||||
const sName = (s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString();
|
||||
return sName && matchStationNameStrict(sName, currentStationName);
|
||||
});
|
||||
|
||||
const rawDownstream = startIdx !== -1 ? stopsList.slice(startIdx + 1) : stopsList;
|
||||
@@ -976,7 +989,7 @@ const AFLDepartsQuais = () => {
|
||||
rawDownstream.forEach(s => {
|
||||
const name = (s.gare_nom || s.nom || s.name || s.station_name || s.stop_name || (typeof s === 'string' ? s : '')).toString().trim();
|
||||
const norm = normalizeName(name);
|
||||
if (name && norm !== normCurrent && !seen.has(norm)) {
|
||||
if (name && !matchStationNameStrict(name, currentStationName) && !seen.has(norm)) {
|
||||
seen.add(norm);
|
||||
downstreamStops.push(name);
|
||||
}
|
||||
|
||||
@@ -248,11 +248,25 @@
|
||||
|
||||
.aff-classiques-arrivees__status-delayed {
|
||||
color: var(--yellow);
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
font-family: "Achemine", sans-serif;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__status-delayed-label {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--yellow);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__status-delayed-time {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
.aff-classiques-arrivees__status-suppressed {
|
||||
|
||||
@@ -6,9 +6,23 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
||||
import { fetchAllRows } from '../../../lib/fetchAllRows';
|
||||
import { getSourceFromPath } from '../../../services/railwayService';
|
||||
import { getSncfTimetable } from '../../../services/sncfApi';
|
||||
import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi';
|
||||
import { matchStationNameStrict } from '../../../services/gtfsStations';
|
||||
import { getTrainLogoAndMeta } from '../../../utils/trainLogoHelper';
|
||||
|
||||
// Pictogramme Autocar SNCF / MOBIGO
|
||||
const AutocarIcon = ({ size = '50px', color = '#ffffff', style = {} }) => (
|
||||
<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)
|
||||
const formatTimeNoSeconds = (v) => {
|
||||
if (v == null) return '';
|
||||
@@ -212,12 +226,8 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
||||
if (!gerr && gdata && gdata.length) {
|
||||
const exactMatch = gdata.find(g => {
|
||||
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
return n1 === n2;
|
||||
});
|
||||
const chosen = exactMatch || gdata[0];
|
||||
const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
|
||||
const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
|
||||
fetchedType = chosen ? chosen.type : null;
|
||||
fetchedId = chosen ? chosen.id : null;
|
||||
}
|
||||
@@ -650,21 +660,20 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
const stopForStation = (stops || []).find(s => {
|
||||
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
|
||||
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
|
||||
const ns = normalizeName(nm || '');
|
||||
const target = normalizeName(targetStationName || '');
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
|
||||
}) || null;
|
||||
horaire._status = displayStatus;
|
||||
horaire._displayTime = displayTime;
|
||||
// utiliser quai en priorité
|
||||
horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay)) || horaire.quai || horaire.quai_reel || horaire.platform || '';
|
||||
let platformVal = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || horaire.quai || horaire.quai_reel || horaire.platform || horaire.voie || '';
|
||||
if (!platformVal) {
|
||||
platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
|
||||
}
|
||||
horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return horaire;
|
||||
});
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
|
||||
|
||||
useEffect(() => {
|
||||
// Alternance toutes les 4 secondes (type+numéro ↔ statut)
|
||||
const id = setInterval(() => setShowStatus(s => !s), 4000);
|
||||
// Alternance toutes les 2 secondes (type+numéro ↔ statut / suppression)
|
||||
const id = setInterval(() => setShowStatus(s => !s), 2000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
@@ -707,11 +716,13 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
} else {
|
||||
const formatted = (sncfData || []).map(t => {
|
||||
const timeStr = t.heure_arrivee || t.heure_depart || t.time || '';
|
||||
const arrHM = (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr);
|
||||
const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return {
|
||||
...t,
|
||||
_displayTime: formatTimeNoSeconds(timeStr),
|
||||
_displayPlatform: t.quai || t.voie || '',
|
||||
_arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr),
|
||||
_displayPlatform: plat,
|
||||
_arrHM: arrHM,
|
||||
_status: t._status || {
|
||||
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
|
||||
detail: '',
|
||||
@@ -720,8 +731,10 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
}
|
||||
};
|
||||
});
|
||||
setHoraires(formatted);
|
||||
setAllHoraires(formatted);
|
||||
const maxCount = defilementMode ? 20 : 10;
|
||||
const limited = (formatted || []).slice(0, maxCount);
|
||||
setHoraires(limited);
|
||||
setAllHoraires(limited);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) setError(e.message || String(e));
|
||||
@@ -791,15 +804,14 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
// Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
|
||||
let displayHoraires = parsedHoraires.slice();
|
||||
if (gareDisplay) {
|
||||
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
|
||||
displayHoraires = displayHoraires.filter(h => {
|
||||
// Exclure si la gare ciblée est la gare de départ
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString());
|
||||
if (departName === normalized) return false;
|
||||
const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString();
|
||||
if (matchStationNameStrict(departName, gareDisplay)) return false;
|
||||
|
||||
// Inclure si gare d'arrivée correspond
|
||||
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (destName === normalized) return true;
|
||||
const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
|
||||
if (matchStationNameStrict(destName, gareDisplay)) return true;
|
||||
|
||||
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le point de départ (isFirst ou index==0)
|
||||
const stopsList = getStops(h);
|
||||
@@ -812,11 +824,11 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
}
|
||||
}
|
||||
for (const s of stopsList) {
|
||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
|
||||
const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString();
|
||||
if (!stopName) continue;
|
||||
const isOrigine = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && minIdx != null && Number(s.index) === minIdx));
|
||||
if (isOrigine) continue;
|
||||
if (stopName === normalized) return true;
|
||||
if (matchStationNameStrict(stopName, gareDisplay)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -889,7 +901,7 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
const stopsListForH = getStops(h);
|
||||
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
|
||||
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null;
|
||||
const stop = byId || byName;
|
||||
if (stop && (stop.heure_arrivee || stop.time || stop.heure)) raw = stop.heure_arrivee || stop.time || stop.heure;
|
||||
}
|
||||
@@ -1165,10 +1177,13 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
// Création des lignes vides pour créer l'espace
|
||||
const emptyRows = Array.from({ length: 10 }).map((_, i) => ({ _isEmpty: true, id: `empty-${i}` }));
|
||||
|
||||
const maxRowsCount = defilementMode ? 20 : 10;
|
||||
const limitedHoraires = (horaires || []).slice(0, maxRowsCount);
|
||||
|
||||
// Liste de lignes à rendre (horaires + espace + horaires + espace en mode défilement pour boucle infinie)
|
||||
const rowsToRender = defilementMode
|
||||
? [...horaires, ...emptyRows, ...horaires, ...emptyRows]
|
||||
: horaires;
|
||||
? [...limitedHoraires, ...emptyRows, ...limitedHoraires, ...emptyRows]
|
||||
: limitedHoraires;
|
||||
|
||||
// Calcul dynamique de l'animation CSS
|
||||
useEffect(() => {
|
||||
@@ -1259,7 +1274,21 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||
const isSuppressed = status && status.label === 'supprimé';
|
||||
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
|
||||
const showPlatform = !isSuppressed && Boolean(cleanPlatform);
|
||||
const isCar = (
|
||||
(horaire.type_train && (horaire.type_train.toUpperCase().includes('AUTOCAR') || horaire.type_train.toUpperCase().includes('CAR TER') || horaire.type_train.toUpperCase() === 'BUS')) ||
|
||||
(horaire.op && (horaire.op.toUpperCase().includes('AUTOCAR') || horaire.op.toUpperCase().includes('CAR TER') || horaire.op.toUpperCase() === 'BUS')) ||
|
||||
(horaire.type && (horaire.type.toUpperCase().includes('AUTOCAR') || horaire.type.toUpperCase().includes('CAR TER') || horaire.type.toUpperCase() === 'BUS')) ||
|
||||
Boolean(horaire.is_substitution) ||
|
||||
(numTrain && String(numTrain).startsWith('3') && String(numTrain).length >= 5) ||
|
||||
typeLabel === 'AUTOCAR' ||
|
||||
rawPlatform === 'AUTOCAR' ||
|
||||
rawPlatform === 'CAR' ||
|
||||
rawPlatform === 'BUS' ||
|
||||
cleanPlatform === 'AUTOCAR' ||
|
||||
cleanPlatform === 'CAR' ||
|
||||
cleanPlatform === 'BUS'
|
||||
);
|
||||
const showPlatform = !isSuppressed && (isCar || Boolean(cleanPlatform));
|
||||
|
||||
const stopsList = isExtended ? getStopsListForArrivees(horaire, gareDisplay || gareParam, normalizeName) : [];
|
||||
|
||||
@@ -1280,7 +1309,10 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
isSuppressed ? (
|
||||
<div className="aff-classiques-arrivees__status-suppressed">supprimé</div>
|
||||
) : 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>
|
||||
)
|
||||
@@ -1293,7 +1325,12 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
</div>
|
||||
|
||||
<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 className="aff-classiques-arrivees__col-destination">
|
||||
@@ -1303,7 +1340,11 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
<div className="aff-classiques-arrivees__col-platform">
|
||||
{showPlatform ? (
|
||||
<div className="aff-classiques-arrivees__platform-box">
|
||||
{isCar ? (
|
||||
<AutocarIcon size="48px" />
|
||||
) : (
|
||||
<div className="aff-classiques-arrivees__platform-text">{cleanPlatform}</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1346,7 +1387,10 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
isSuppressed ? (
|
||||
<div className="aff-classiques-arrivees__status-suppressed">supprimé</div>
|
||||
) : 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>
|
||||
)
|
||||
@@ -1360,7 +1404,12 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
|
||||
{/* COL 3 : Heure (grande, jaune) */}
|
||||
<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>
|
||||
|
||||
{/* COL 4 : Provenance */}
|
||||
@@ -1372,7 +1421,11 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
<div className="aff-classiques-arrivees__col-platform">
|
||||
{showPlatform ? (
|
||||
<div className="aff-classiques-arrivees__platform-box">
|
||||
{isCar ? (
|
||||
<AutocarIcon size="48px" />
|
||||
) : (
|
||||
<div className="aff-classiques-arrivees__platform-text">{cleanPlatform}</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -248,11 +248,25 @@
|
||||
|
||||
.aff-classiques-departs__status-delayed {
|
||||
color: var(--yellow);
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
font-family: "Achemine", sans-serif;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__status-delayed-label {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--yellow);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.aff-classiques-departs__status-delayed-time {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
.aff-classiques-departs__status-suppressed {
|
||||
|
||||
@@ -6,9 +6,23 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime';
|
||||
import useAutoAnnonce from '../../../hooks/useAutoAnnonce';
|
||||
import { fetchAllRows } from '../../../lib/fetchAllRows';
|
||||
import { getSourceFromPath } from '../../../services/railwayService';
|
||||
import { getSncfTimetable } from '../../../services/sncfApi';
|
||||
import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi';
|
||||
import { matchStationNameStrict } from '../../../services/gtfsStations';
|
||||
import { getTrainLogoAndMeta } from '../../../utils/trainLogoHelper';
|
||||
|
||||
// Pictogramme Autocar SNCF / MOBIGO
|
||||
const AutocarIcon = ({ size = '50px', color = '#ffffff', style = {} }) => (
|
||||
<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)
|
||||
const formatTimeNoSeconds = (v) => {
|
||||
if (v == null) return '';
|
||||
@@ -212,12 +226,8 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`;
|
||||
const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle);
|
||||
if (!gerr && gdata && gdata.length) {
|
||||
const exactMatch = gdata.find(g => {
|
||||
const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim();
|
||||
return n1 === n2;
|
||||
});
|
||||
const chosen = exactMatch || gdata[0];
|
||||
const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
|
||||
const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
|
||||
fetchedType = chosen ? chosen.type : null;
|
||||
fetchedId = chosen ? chosen.id : null;
|
||||
}
|
||||
@@ -650,21 +660,20 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
const stopForStation = (stops || []).find(s => {
|
||||
const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null;
|
||||
const nm = s.gare_nom || s.nom || s.name || s.stop_name || '';
|
||||
const ns = normalizeName(nm || '');
|
||||
const target = normalizeName(targetStationName || '');
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target);
|
||||
return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName));
|
||||
}) || null;
|
||||
horaire._status = displayStatus;
|
||||
horaire._displayTime = displayTime;
|
||||
// utiliser quai en priorité
|
||||
horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay)) || horaire.quai || horaire.quai_reel || horaire.platform || '';
|
||||
let platformVal = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || horaire.quai || horaire.quai_reel || horaire.platform || horaire.voie || '';
|
||||
if (!platformVal) {
|
||||
platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam);
|
||||
}
|
||||
horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return horaire;
|
||||
});
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]);
|
||||
}, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]);
|
||||
|
||||
useEffect(() => {
|
||||
// Alternance toutes les 4 secondes (type+numéro ↔ statut)
|
||||
const id = setInterval(() => setShowStatus(s => !s), 4000);
|
||||
// Alternance toutes les 2 secondes (type+numéro ↔ statut / suppression)
|
||||
const id = setInterval(() => setShowStatus(s => !s), 2000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
@@ -705,20 +714,26 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
if (sncfErr) {
|
||||
setError(sncfErr);
|
||||
} else {
|
||||
const formatted = (sncfData || []).map(t => ({
|
||||
const formatted = (sncfData || []).map(t => {
|
||||
const depHM = (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart);
|
||||
const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
return {
|
||||
...t,
|
||||
_displayTime: formatTimeNoSeconds(t.heure_depart),
|
||||
_displayPlatform: t.quai || t.voie || '',
|
||||
_depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart),
|
||||
_displayPlatform: plat,
|
||||
_depHM: depHM,
|
||||
_status: t._status || {
|
||||
label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"),
|
||||
detail: '',
|
||||
delayMinutes: t.retard_minutes || null,
|
||||
causeDetails: t.cause_retard || null
|
||||
}
|
||||
}));
|
||||
setHoraires(formatted);
|
||||
setAllHoraires(formatted);
|
||||
};
|
||||
});
|
||||
const maxCount = defilementMode ? 20 : 10;
|
||||
const limited = (formatted || []).slice(0, maxCount);
|
||||
setHoraires(limited);
|
||||
setAllHoraires(limited);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) setError(e.message || String(e));
|
||||
@@ -788,15 +803,14 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
// Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus)
|
||||
let displayHoraires = parsedHoraires.slice();
|
||||
if (gareDisplay) {
|
||||
const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' '));
|
||||
displayHoraires = displayHoraires.filter(h => {
|
||||
// Exclure si la gare ciblée est la gare d'arrivée finale
|
||||
const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString());
|
||||
if (destName === normalized) return false;
|
||||
const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
|
||||
if (matchStationNameStrict(destName, gareDisplay)) return false;
|
||||
|
||||
// Inclure si gare de départ correspond
|
||||
const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString());
|
||||
if (departName === normalized) return true;
|
||||
const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString();
|
||||
if (matchStationNameStrict(departName, gareDisplay)) return true;
|
||||
|
||||
// Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max)
|
||||
const stopsList = getStops(h);
|
||||
@@ -809,11 +823,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
}
|
||||
}
|
||||
for (const s of stopsList) {
|
||||
const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString());
|
||||
const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString();
|
||||
if (!stopName) continue;
|
||||
const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx));
|
||||
if (isTerminus) continue;
|
||||
if (stopName === normalized) return true;
|
||||
if (matchStationNameStrict(stopName, gareDisplay)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -886,7 +900,7 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
const stopsListForH = getStops(h);
|
||||
if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) {
|
||||
const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null;
|
||||
const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null;
|
||||
const stop = byId || byName;
|
||||
if (stop && (stop.heure_depart || stop.time || stop.heure)) raw = stop.heure_depart || stop.time || stop.heure;
|
||||
}
|
||||
@@ -1162,10 +1176,13 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
// Création des lignes vides pour créer l'espace
|
||||
const emptyRows = Array.from({ length: 10 }).map((_, i) => ({ _isEmpty: true, id: `empty-${i}` }));
|
||||
|
||||
const maxRowsCount = defilementMode ? 20 : 10;
|
||||
const limitedHoraires = (horaires || []).slice(0, maxRowsCount);
|
||||
|
||||
// Liste de lignes à rendre (horaires + espace + horaires + espace en mode défilement pour boucle infinie)
|
||||
const rowsToRender = defilementMode
|
||||
? [...horaires, ...emptyRows, ...horaires, ...emptyRows]
|
||||
: horaires;
|
||||
? [...limitedHoraires, ...emptyRows, ...limitedHoraires, ...emptyRows]
|
||||
: limitedHoraires;
|
||||
|
||||
// Calcul dynamique de l'animation CSS
|
||||
useEffect(() => {
|
||||
@@ -1256,7 +1273,21 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||
const isSuppressed = status && status.label === 'supprimé';
|
||||
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
|
||||
const showPlatform = !isSuppressed && Boolean(cleanPlatform);
|
||||
const isCar = (
|
||||
(horaire.type_train && (horaire.type_train.toUpperCase().includes('AUTOCAR') || horaire.type_train.toUpperCase().includes('CAR TER') || horaire.type_train.toUpperCase() === 'BUS')) ||
|
||||
(horaire.op && (horaire.op.toUpperCase().includes('AUTOCAR') || horaire.op.toUpperCase().includes('CAR TER') || horaire.op.toUpperCase() === 'BUS')) ||
|
||||
(horaire.type && (horaire.type.toUpperCase().includes('AUTOCAR') || horaire.type.toUpperCase().includes('CAR TER') || horaire.type.toUpperCase() === 'BUS')) ||
|
||||
Boolean(horaire.is_substitution) ||
|
||||
(numTrain && String(numTrain).startsWith('3') && String(numTrain).length >= 5) ||
|
||||
typeLabel === 'AUTOCAR' ||
|
||||
rawPlatform === 'AUTOCAR' ||
|
||||
rawPlatform === 'CAR' ||
|
||||
rawPlatform === 'BUS' ||
|
||||
cleanPlatform === 'AUTOCAR' ||
|
||||
cleanPlatform === 'CAR' ||
|
||||
cleanPlatform === 'BUS'
|
||||
);
|
||||
const showPlatform = !isSuppressed && (isCar || Boolean(cleanPlatform));
|
||||
|
||||
const stopsList = isExtended ? getStopsListForDeparts(horaire, gareDisplay || gareParam, normalizeName) : [];
|
||||
|
||||
@@ -1277,7 +1308,10 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
isSuppressed ? (
|
||||
<div className="aff-classiques-departs__status-suppressed">supprimé</div>
|
||||
) : 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>
|
||||
)
|
||||
@@ -1290,7 +1324,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
</div>
|
||||
|
||||
<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 className="aff-classiques-departs__col-destination">
|
||||
@@ -1300,7 +1339,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
<div className="aff-classiques-departs__col-platform">
|
||||
{showPlatform ? (
|
||||
<div className="aff-classiques-departs__platform-box">
|
||||
{isCar ? (
|
||||
<AutocarIcon size="48px" />
|
||||
) : (
|
||||
<div className="aff-classiques-departs__platform-text">{cleanPlatform}</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1343,7 +1386,10 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
isSuppressed ? (
|
||||
<div className="aff-classiques-departs__status-suppressed">supprimé</div>
|
||||
) : 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>
|
||||
)
|
||||
@@ -1357,7 +1403,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
|
||||
{/* COL 3 : Heure (grande, jaune) */}
|
||||
<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>
|
||||
|
||||
{/* COL 4 : Destination */}
|
||||
@@ -1369,7 +1420,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
<div className="aff-classiques-departs__col-platform">
|
||||
{showPlatform ? (
|
||||
<div className="aff-classiques-departs__platform-box">
|
||||
{isCar ? (
|
||||
<AutocarIcon size="48px" />
|
||||
) : (
|
||||
<div className="aff-classiques-departs__platform-text">{cleanPlatform}</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,11 @@ body, html {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__left--tgv {
|
||||
bottom: 240px !important;
|
||||
padding: 32px 48px 20px 48px !important;
|
||||
}
|
||||
|
||||
/* Logo Dynamique (TER Bleu / Opérateur) */
|
||||
.aff-classique-quai-arr__brand {
|
||||
display: flex;
|
||||
@@ -65,6 +70,7 @@ body, html {
|
||||
.aff-classique-quai-arr__time-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
min-height: 100px;
|
||||
margin-bottom: 8px;
|
||||
z-index: 3;
|
||||
}
|
||||
@@ -85,12 +91,18 @@ body, html {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__time--suppressed,
|
||||
.aff-classique-quai-arr__status-alt {
|
||||
font-weight: 700;
|
||||
font-size: 68px;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__status--suppressed {
|
||||
color: #d32f2f !important;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__time--delayed,
|
||||
.aff-classique-quai-arr__status--delayed {
|
||||
color: #f5a623 !important;
|
||||
}
|
||||
@@ -107,6 +119,22 @@ body, html {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__destination--multi {
|
||||
font-size: 52px !important;
|
||||
margin-top: 6px !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
/* Services PMR, Vélo, Bar */
|
||||
.aff-classique-quai-arr__services {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
margin-top: auto;
|
||||
margin-bottom: 8px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
/* Info Train (Type + Numéro) */
|
||||
.aff-classique-quai-arr__train-info {
|
||||
display: flex;
|
||||
@@ -173,6 +201,10 @@ body, html {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__right--tgv {
|
||||
bottom: 240px !important;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__stops-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -258,6 +290,74 @@ body, html {
|
||||
font-size: 44px;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
FOURCHE / BIFURCATION DESSERTE TGV (Conforme Image 2)
|
||||
========================================================= */
|
||||
.aff-classique-quai-arr__fork-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__fork-split-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-height: 100px;
|
||||
padding-left: 65px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Rail courbe violet (#5c538f) */
|
||||
.aff-classique-quai-arr__fork-curve-rail {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: -15px;
|
||||
width: 125px;
|
||||
height: 80px;
|
||||
border-left: 28px solid #5c538f;
|
||||
border-bottom: 28px solid #5c538f;
|
||||
border-bottom-left-radius: 35px;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Branche droite */
|
||||
.aff-classique-quai-arr__fork-branch-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 115px;
|
||||
margin-top: 10px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* Libellé "voitures 11 à 18" */
|
||||
.aff-classique-quai-arr__fork-coaches-label {
|
||||
color: #f5a623;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Boîte terminus branche droite */
|
||||
.aff-classique-quai-arr__fork-terminus-box {
|
||||
background-color: #5c538f;
|
||||
padding: 8px 30px 8px 20px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.aff-classique-quai-arr__fork-terminus-name {
|
||||
color: #ffffff;
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
FOOTER & HORLOGE (Conforme Image 2)
|
||||
========================================================= */
|
||||
|
||||
@@ -6,10 +6,34 @@ import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime
|
||||
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
|
||||
import { getSourceFromPath } from '../../../../services/railwayService';
|
||||
import { getSncfTimetable } from '../../../../services/sncfApi';
|
||||
import { matchStationNameStrict } from '../../../../services/gtfsStations';
|
||||
import GareVideoprotection from '../../../../components/afficheurs/GareVideoprotection';
|
||||
import AttentifsEnsemble from '../../../../components/afficheurs/AttentifsEnsemble';
|
||||
import ClassiquesInformations from '../ClassiquesInformations';
|
||||
import ClassiquesArrivees from '../ClassiquesArrivees';
|
||||
import TgvCompositionQuai from '../../../../components/afficheurs/TgvCompositionQuai';
|
||||
|
||||
function TgvServiceIcons() {
|
||||
return (
|
||||
<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() {
|
||||
const [now, setNow] = useState(new Date());
|
||||
@@ -131,6 +155,7 @@ export default function ClassiquesArriveesQuais() {
|
||||
const [, setHoraireError] = useState(null);
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s)
|
||||
const [showStatusAlt, setShowStatusAlt] = useState(false);
|
||||
|
||||
const pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase();
|
||||
const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true';
|
||||
@@ -144,6 +169,12 @@ export default function ClassiquesArriveesQuais() {
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
// Alternance 3 secondes : Heure normale ↔ Statut (pour trains retardés ou supprimés)
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setShowStatusAlt(prev => !prev), 3000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
// Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai
|
||||
useEffect(() => {
|
||||
if (selectedHoraire) return;
|
||||
@@ -315,14 +346,29 @@ export default function ClassiquesArriveesQuais() {
|
||||
if (sncfData && sncfData.length > 0) {
|
||||
let matchingTrains = sncfData;
|
||||
if (targetQuaiClean) {
|
||||
matchingTrains = sncfData.filter(t => cleanPlatform(t.quai || t.voie || t.quai_reel || t._displayPlatform) === targetQuaiClean);
|
||||
matchingTrains = sncfData.filter(t => cleanPlatform(t._displayPlatform || t.quai || t.voie || t.quai_reel) === targetQuaiClean);
|
||||
}
|
||||
const train = matchingTrains[0];
|
||||
if (train) {
|
||||
const arrHM = parseTimeHM(train.heure_arrivee || train.time || train.heure);
|
||||
const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || '';
|
||||
const trainNumClean = (train.numero_train || train.num || '').toString();
|
||||
|
||||
// Recherche d'une deuxième tranche / train couplé au même horaire
|
||||
const partner = matchingTrains.find(t => {
|
||||
if (!t || t === train) return false;
|
||||
const tNum = (t.numero_train || t.num || '').toString();
|
||||
const tHM = parseTimeHM(t.heure_arrivee || t.time || t.heure);
|
||||
return tNum !== trainNumClean && tHM === arrHM;
|
||||
});
|
||||
|
||||
chosenTrain = {
|
||||
horaire: {
|
||||
...train,
|
||||
quai: trainPlatform,
|
||||
quai_reel: trainPlatform,
|
||||
voie: trainPlatform,
|
||||
_displayPlatform: trainPlatform,
|
||||
_status: train._status || {
|
||||
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
|
||||
detail: '',
|
||||
@@ -330,10 +376,11 @@ export default function ClassiquesArriveesQuais() {
|
||||
causeDetails: train.cause_retard || null
|
||||
}
|
||||
},
|
||||
partner: partner || null,
|
||||
stop: {
|
||||
gare_nom: train.gare_arrivee_nom || gareDisplay || gareParam,
|
||||
heure_arrivee: train.heure_arrivee || train.time || train.heure,
|
||||
quai: train.quai || train.voie || quaiParam
|
||||
quai: trainPlatform
|
||||
},
|
||||
arrHM: arrHM != null ? arrHM : 0,
|
||||
rawTime: train.heure_arrivee || train.time || train.heure
|
||||
@@ -353,13 +400,14 @@ export default function ClassiquesArriveesQuais() {
|
||||
} else if (gareDisplay) {
|
||||
const { data: gdata } = await supabase.from('gares').select('id,nom').ilike('nom', `%${gareDisplay}%`);
|
||||
if (gdata && gdata.length) {
|
||||
setResolvedGareId(gdata[0].id);
|
||||
const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
|
||||
const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
|
||||
if (chosen) setResolvedGareId(chosen.id);
|
||||
}
|
||||
}
|
||||
|
||||
const { data: all, error } = await fetchAllRows(supabase.from('horaires').select('*'));
|
||||
if (!error && Array.isArray(all) && all.length > 0) {
|
||||
const normalizedTarget = gareDisplay ? normalizeName(gareDisplay) : null;
|
||||
const now = new Date();
|
||||
const tomorrow = new Date(now);
|
||||
tomorrow.setDate(now.getDate() + 1);
|
||||
@@ -375,14 +423,14 @@ export default function ClassiquesArriveesQuais() {
|
||||
const stops = getStops(h);
|
||||
let stopMatch = null;
|
||||
|
||||
if (normalizedTarget) {
|
||||
const departNameNorm = normalizeName(h.gare_depart_nom || h.gare_depart || '');
|
||||
if (departNameNorm && departNameNorm === normalizedTarget) {
|
||||
if (gareDisplay) {
|
||||
const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
|
||||
if (matchStationNameStrict(departName, gareDisplay)) {
|
||||
// Target is the departure origin -> not an arrival here
|
||||
continue;
|
||||
}
|
||||
const arriveeName = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || '');
|
||||
if (arriveeName && arriveeName === normalizedTarget) {
|
||||
const arriveeName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
|
||||
if (matchStationNameStrict(arriveeName, gareDisplay)) {
|
||||
stopMatch = {
|
||||
heure_arrivee: h.heure_arrivee || h.time || h.heure,
|
||||
gare_nom: h.gare_arrivee_nom || h.gare_arrivee,
|
||||
@@ -391,8 +439,8 @@ export default function ClassiquesArriveesQuais() {
|
||||
}
|
||||
if (!stopMatch && Array.isArray(stops)) {
|
||||
for (const s of stops) {
|
||||
const sName = normalizeName(typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''));
|
||||
if (sName === normalizedTarget) {
|
||||
const sName = (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || '')).toString();
|
||||
if (matchStationNameStrict(sName, gareDisplay)) {
|
||||
stopMatch = s;
|
||||
break;
|
||||
}
|
||||
@@ -408,7 +456,11 @@ export default function ClassiquesArriveesQuais() {
|
||||
|
||||
if (!stopMatch) continue;
|
||||
|
||||
const platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || '';
|
||||
const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_arrivee || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_arrivee || h.time || h.heure;
|
||||
const arrHM = parseTimeHM(rawTime);
|
||||
if (arrHM == null) continue;
|
||||
|
||||
let platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || '';
|
||||
const platClean = cleanPlatform(platform);
|
||||
|
||||
if (targetQuaiClean) {
|
||||
@@ -417,10 +469,6 @@ export default function ClassiquesArriveesQuais() {
|
||||
}
|
||||
}
|
||||
|
||||
const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_arrivee || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_arrivee || h.time || h.heure;
|
||||
const arrHM = parseTimeHM(rawTime);
|
||||
if (arrHM == null) continue;
|
||||
|
||||
candidates.push({
|
||||
horaire: h,
|
||||
stop: stopMatch,
|
||||
@@ -448,7 +496,16 @@ export default function ClassiquesArriveesQuais() {
|
||||
: (filteredCandidates.length > 0 ? filteredCandidates.sort((a, b) => a.arrHM - b.arrHM)[0] : null));
|
||||
|
||||
if (chosen) {
|
||||
chosenTrain = chosen;
|
||||
const partnerCand = filteredCandidates.find(c => {
|
||||
if (!c || c === chosen) return false;
|
||||
const cNum = (c.horaire.numero_train || c.horaire.num || '').toString();
|
||||
const chosenNum = (chosen.horaire.numero_train || chosen.horaire.num || '').toString();
|
||||
return cNum !== chosenNum && c.arrHM === chosen.arrHM;
|
||||
});
|
||||
chosenTrain = {
|
||||
...chosen,
|
||||
partner: partnerCand ? partnerCand.horaire : null
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -501,8 +558,7 @@ export default function ClassiquesArriveesQuais() {
|
||||
|
||||
// Filtrer les arrêts jusqu'à la gare courante
|
||||
if (gareDisplay && stopsList.length > 0) {
|
||||
const normCur = normalizeName(gareDisplay);
|
||||
const foundIdx = stopsList.findIndex(s => normalizeName(s) === normCur);
|
||||
const foundIdx = stopsList.findIndex(s => matchStationNameStrict(s, gareDisplay));
|
||||
if (foundIdx > 0) {
|
||||
stopsList = stopsList.slice(0, foundIdx + 1);
|
||||
}
|
||||
@@ -510,9 +566,8 @@ export default function ClassiquesArriveesQuais() {
|
||||
|
||||
// Garantir que le terminus est présent en dernier
|
||||
if (destinationName) {
|
||||
const destNorm = normalizeName(destinationName);
|
||||
const lastNorm = stopsList.length > 0 ? normalizeName(stopsList[stopsList.length - 1]) : '';
|
||||
if (lastNorm !== destNorm) {
|
||||
const lastStop = stopsList.length > 0 ? stopsList[stopsList.length - 1] : '';
|
||||
if (!matchStationNameStrict(lastStop, destinationName)) {
|
||||
stopsList.push(destinationName);
|
||||
}
|
||||
}
|
||||
@@ -545,10 +600,64 @@ export default function ClassiquesArriveesQuais() {
|
||||
}
|
||||
}
|
||||
|
||||
const partner = selectedHoraire?.partner;
|
||||
const destinationName1 = destinationName;
|
||||
const destinationName2 = partner ? (partner.gare_arrivee_nom || partner.gare_arrivee || partner.destination || partner.dest || '') : (horaire?.second_destination || horaire?.dest2 || horaire?.destination2 || (horaire?.tranches && horaire?.tranches[1]?.destination) || '');
|
||||
|
||||
const trainType1 = trainType;
|
||||
const trainType2 = partner ? (partner.type_train || partner.train_type || partner.type || partner.op || 'TGV') : (horaire?.type_train2 || horaire?.train_type2 || (destinationName2 ? trainType : ''));
|
||||
|
||||
const trainNumber1 = trainNumber;
|
||||
const trainNumber2 = partner ? (partner.numero_train || partner.numero || partner.num || '') : (horaire?.numero_train2 || horaire?.num2 || (horaire?.tranches && horaire?.tranches[1]?.num) || '');
|
||||
|
||||
const isDifferentDest = Boolean(destinationName1 && destinationName2 && !matchStationNameStrict(destinationName1, destinationName2));
|
||||
|
||||
// Récupération des arrêts de la deuxième tranche
|
||||
let stopsList2 = [];
|
||||
if (partner) {
|
||||
const rawStops2 = getStops(partner);
|
||||
if (Array.isArray(rawStops2) && rawStops2.length > 0) {
|
||||
stopsList2 = rawStops2.map(s => (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''))).filter(Boolean);
|
||||
}
|
||||
if (gareDisplay && stopsList2.length > 0) {
|
||||
const fIdx = stopsList2.findIndex(s => matchStationNameStrict(s, gareDisplay));
|
||||
if (fIdx >= 0 && fIdx < stopsList2.length - 1) {
|
||||
stopsList2 = stopsList2.slice(fIdx + 1);
|
||||
}
|
||||
}
|
||||
if (destinationName2 && !matchStationNameStrict(stopsList2[stopsList2.length - 1] || '', destinationName2)) {
|
||||
stopsList2.push(destinationName2);
|
||||
}
|
||||
}
|
||||
|
||||
// Calcul du tronc commun et des branches de séparation (Fourche Image 2)
|
||||
let commonStops = [];
|
||||
let branch1Stops = [];
|
||||
let branch2Stops = [];
|
||||
|
||||
if (isDifferentDest && stopsList.length > 0 && stopsList2.length > 0) {
|
||||
let divIdx = 0;
|
||||
const minLen = Math.min(stopsList.length, stopsList2.length);
|
||||
while (divIdx < minLen && matchStationNameStrict(stopsList[divIdx], stopsList2[divIdx])) {
|
||||
commonStops.push(stopsList[divIdx]);
|
||||
divIdx++;
|
||||
}
|
||||
branch1Stops = stopsList.slice(divIdx);
|
||||
branch2Stops = stopsList2.slice(divIdx);
|
||||
}
|
||||
|
||||
const isTgv = Boolean(
|
||||
(trainType1 || '').toUpperCase().includes('TGV') ||
|
||||
(trainType2 || '').toUpperCase().includes('TGV') ||
|
||||
(operator || '').toUpperCase().includes('TGV') ||
|
||||
(brand && (brand.label || '').toUpperCase().includes('TGV')) ||
|
||||
(horaire && ((horaire.type_train || horaire.type || '').toUpperCase().includes('TGV') || (horaire.op || '').toUpperCase().includes('TGV')))
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="aff-classique-quai-arr__container">
|
||||
{/* 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 */}
|
||||
<div className="aff-classique-quai-arr__brand">
|
||||
<img src={brand.logo} alt={brand.label} className="aff-classique-quai-arr__brand-logo" />
|
||||
@@ -559,35 +668,120 @@ export default function ClassiquesArriveesQuais() {
|
||||
|
||||
{/* Heure et Statut */}
|
||||
<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' : ''}`}>
|
||||
{formattedTime || '--:--'}
|
||||
</span>
|
||||
<span className={`aff-classique-quai-arr__status ${isSuppressed ? 'aff-classique-quai-arr__status--suppressed' : ''} ${isDelayed ? 'aff-classique-quai-arr__status--delayed' : ''}`}>
|
||||
{(isSuppressed || isDelayed) ? (
|
||||
showStatusAlt ? (
|
||||
<span className={`aff-classique-quai-arr__status-alt ${isSuppressed ? 'aff-classique-quai-arr__status--suppressed' : 'aff-classique-quai-arr__status--delayed'}`}>
|
||||
{displayStatusText}
|
||||
</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>
|
||||
|
||||
{/* Destination / Gare d'Arrivée Principale */}
|
||||
<div className="aff-classique-quai-arr__destination">
|
||||
{destinationName || 'Aucune arrivée prévue'}
|
||||
<div className={`aff-classique-quai-arr__destination ${isDifferentDest ? 'aff-classique-quai-arr__destination--multi' : ''}`}>
|
||||
{destinationName1 || 'Aucune arrivée prévue'}
|
||||
</div>
|
||||
|
||||
{/* Type de Train & Numéro */}
|
||||
{/* Type de Train & Numéro 1 */}
|
||||
<div className="aff-classique-quai-arr__train-info">
|
||||
{trainType && <span className="aff-classique-quai-arr__train-type">{trainType}</span>}
|
||||
{trainNumber && <span className="aff-classique-quai-arr__train-number">{trainNumber}</span>}
|
||||
{trainType1 && <span className="aff-classique-quai-arr__train-type">{trainType1}</span>}
|
||||
{trainNumber1 && <span className="aff-classique-quai-arr__train-number">{trainNumber1}</span>}
|
||||
</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) */}
|
||||
<div className="aff-classique-quai-arr__watermark" aria-hidden="true">
|
||||
<span>arrivée</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PANNEAU DROIT : VERT FORÊT AVEC DESSERTE (Conforme Image 2) */}
|
||||
<div className="aff-classique-quai-arr__right">
|
||||
{/* PANNEAU DROIT : VERT FORÊT AVEC DESSERTE (Conforme Image 2 & Fourche) */}
|
||||
<div className={`aff-classique-quai-arr__right ${isTgv ? 'aff-classique-quai-arr__right--tgv' : ''}`}>
|
||||
<div className="aff-classique-quai-arr__stops-wrapper">
|
||||
{stopsList.length > 0 ? (
|
||||
{isDifferentDest && commonStops.length > 0 ? (
|
||||
/* AFFICHAGE FOURCHE / BIFURCATION */
|
||||
<div className="aff-classique-quai-arr__fork-container">
|
||||
{/* Tronc commun */}
|
||||
<ul className="aff-classique-quai-arr__stops-list">
|
||||
{commonStops.map((stopName, idx) => (
|
||||
<li key={idx} className="aff-classique-quai-arr__stop-item">
|
||||
<span className="aff-classique-quai-arr__bullet" />
|
||||
<span className="aff-classique-quai-arr__stop-name">{stopName}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Ligne d'embranchement avec courbe */}
|
||||
<div className="aff-classique-quai-arr__fork-split-row">
|
||||
<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>
|
||||
) : (
|
||||
/* 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;
|
||||
@@ -608,19 +802,30 @@ export default function ClassiquesArriveesQuais() {
|
||||
<div style={{ color: '#ffffff', fontSize: '38px', fontWeight: 600, padding: '40px' }}>
|
||||
Veuillez consulter les écrans du hall de la gare.
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BANDEAU BAS : PLEINE LARGEUR & HORLOGE (Conforme Image 2) */}
|
||||
{/* BANDEAU BAS : TGV COMPOSITION OU BANDEAU STANDARD */}
|
||||
{isTgv ? (
|
||||
<TgvCompositionQuai
|
||||
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 ? `Voie ${cleanPlatform(horaire.quai).toUpperCase()}` : '')}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@@ -37,6 +37,11 @@ body, html {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__left--tgv {
|
||||
bottom: 240px !important;
|
||||
padding: 32px 48px 20px 48px !important;
|
||||
}
|
||||
|
||||
/* Logo Dynamique */
|
||||
.aff-classique-quai-dep__brand {
|
||||
display: flex;
|
||||
@@ -65,6 +70,7 @@ body, html {
|
||||
.aff-classique-quai-dep__time-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
min-height: 100px;
|
||||
margin-bottom: 8px;
|
||||
z-index: 3;
|
||||
}
|
||||
@@ -85,12 +91,18 @@ body, html {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__time--suppressed,
|
||||
.aff-classique-quai-dep__status-alt {
|
||||
font-weight: 700;
|
||||
font-size: 68px;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__status--suppressed {
|
||||
color: #d32f2f !important;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__time--delayed,
|
||||
.aff-classique-quai-dep__status--delayed {
|
||||
color: #f5a623 !important;
|
||||
}
|
||||
@@ -107,6 +119,22 @@ body, html {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__destination--multi {
|
||||
font-size: 52px !important;
|
||||
margin-top: 6px !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
/* Services PMR, Vélo, Bar */
|
||||
.aff-classique-quai-dep__services {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
margin-top: auto;
|
||||
margin-bottom: 8px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
/* Info Train (Type + Numéro) */
|
||||
.aff-classique-quai-dep__train-info {
|
||||
display: flex;
|
||||
@@ -173,6 +201,10 @@ body, html {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__right--tgv {
|
||||
bottom: 240px !important;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__stops-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -258,6 +290,74 @@ body, html {
|
||||
font-size: 44px;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
FOURCHE / BIFURCATION DESSERTE TGV (Conforme Image 2)
|
||||
========================================================= */
|
||||
.aff-classique-quai-dep__fork-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__fork-split-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-height: 100px;
|
||||
padding-left: 65px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Rail courbe violet (#5c538f) */
|
||||
.aff-classique-quai-dep__fork-curve-rail {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: -15px;
|
||||
width: 125px;
|
||||
height: 80px;
|
||||
border-left: 28px solid #5c538f;
|
||||
border-bottom: 28px solid #5c538f;
|
||||
border-bottom-left-radius: 35px;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Branche droite */
|
||||
.aff-classique-quai-dep__fork-branch-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 115px;
|
||||
margin-top: 10px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* Libellé "voitures 11 à 18" */
|
||||
.aff-classique-quai-dep__fork-coaches-label {
|
||||
color: #f5a623;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Boîte terminus branche droite (ex: Lille Europe) */
|
||||
.aff-classique-quai-dep__fork-terminus-box {
|
||||
background-color: #5c538f;
|
||||
padding: 8px 30px 8px 20px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.aff-classique-quai-dep__fork-terminus-name {
|
||||
color: #ffffff;
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
FOOTER & HORLOGE (Conforme Image 1)
|
||||
========================================================= */
|
||||
|
||||
@@ -6,11 +6,35 @@ import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime
|
||||
import useAutoAnnonce from '../../../../hooks/useAutoAnnonce';
|
||||
import { getSourceFromPath } from '../../../../services/railwayService';
|
||||
import { getSncfTimetable } from '../../../../services/sncfApi';
|
||||
import { matchStationNameStrict } from '../../../../services/gtfsStations';
|
||||
import GareVideoprotection from '../../../../components/afficheurs/GareVideoprotection';
|
||||
import AttentifsEnsemble from '../../../../components/afficheurs/AttentifsEnsemble';
|
||||
import DepartEnCoursClassique from '../../../../components/afficheurs/DepartEnCoursClassique';
|
||||
import ClassiquesInformations from '../ClassiquesInformations';
|
||||
import ClassiquesDeparts from '../ClassiquesDeparts';
|
||||
import TgvCompositionQuai from '../../../../components/afficheurs/TgvCompositionQuai';
|
||||
|
||||
function TgvServiceIcons() {
|
||||
return (
|
||||
<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() {
|
||||
const [now, setNow] = useState(new Date());
|
||||
@@ -132,6 +156,7 @@ export default function ClassiquesDepartsQuais() {
|
||||
const [, setHoraireError] = useState(null);
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s)
|
||||
const [showStatusAlt, setShowStatusAlt] = useState(false);
|
||||
|
||||
const pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase();
|
||||
const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true';
|
||||
@@ -147,6 +172,12 @@ export default function ClassiquesDepartsQuais() {
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
// Alternance 3 secondes : Heure normale ↔ Statut (pour trains retardés ou supprimés)
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setShowStatusAlt(prev => !prev), 3000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
// Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai
|
||||
useEffect(() => {
|
||||
if (selectedHoraire) return;
|
||||
@@ -317,14 +348,29 @@ export default function ClassiquesDepartsQuais() {
|
||||
if (sncfData && sncfData.length > 0) {
|
||||
let matchingTrains = sncfData;
|
||||
if (targetQuaiClean) {
|
||||
matchingTrains = sncfData.filter(t => cleanPlatform(t.quai || t.voie || t.quai_reel || t._displayPlatform) === targetQuaiClean);
|
||||
matchingTrains = sncfData.filter(t => cleanPlatform(t._displayPlatform || t.quai || t.voie || t.quai_reel) === targetQuaiClean);
|
||||
}
|
||||
const train = matchingTrains[0];
|
||||
if (train) {
|
||||
const depHM = parseTimeHM(train.heure_depart || train.time || train.heure);
|
||||
const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || '';
|
||||
const trainNumClean = (train.numero_train || train.num || '').toString();
|
||||
|
||||
// Recherche d'une deuxième tranche / train couplé au même horaire
|
||||
const partner = matchingTrains.find(t => {
|
||||
if (!t || t === train) return false;
|
||||
const tNum = (t.numero_train || t.num || '').toString();
|
||||
const tHM = parseTimeHM(t.heure_depart || t.time || t.heure);
|
||||
return tNum !== trainNumClean && tHM === depHM;
|
||||
});
|
||||
|
||||
chosenTrain = {
|
||||
horaire: {
|
||||
...train,
|
||||
quai: trainPlatform,
|
||||
quai_reel: trainPlatform,
|
||||
voie: trainPlatform,
|
||||
_displayPlatform: trainPlatform,
|
||||
_status: train._status || {
|
||||
label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"),
|
||||
detail: '',
|
||||
@@ -332,10 +378,11 @@ export default function ClassiquesDepartsQuais() {
|
||||
causeDetails: train.cause_retard || null
|
||||
}
|
||||
},
|
||||
partner: partner || null,
|
||||
stop: {
|
||||
gare_nom: train.gare_depart_nom || gareDisplay || gareParam,
|
||||
heure_depart: train.heure_depart || train.time || train.heure,
|
||||
quai: train.quai || train.voie || quaiParam
|
||||
quai: trainPlatform
|
||||
},
|
||||
depHM: depHM != null ? depHM : 0,
|
||||
rawTime: train.heure_depart || train.time || train.heure
|
||||
@@ -355,13 +402,14 @@ export default function ClassiquesDepartsQuais() {
|
||||
} else if (gareDisplay) {
|
||||
const { data: gdata } = await supabase.from('gares').select('id,nom').ilike('nom', `%${gareDisplay}%`);
|
||||
if (gdata && gdata.length) {
|
||||
setResolvedGareId(gdata[0].id);
|
||||
const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay));
|
||||
const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null);
|
||||
if (chosen) setResolvedGareId(chosen.id);
|
||||
}
|
||||
}
|
||||
|
||||
const { data: all, error } = await fetchAllRows(supabase.from('horaires').select('*'));
|
||||
if (!error && Array.isArray(all) && all.length > 0) {
|
||||
const normalizedTarget = gareDisplay ? normalizeName(gareDisplay) : null;
|
||||
const now = new Date();
|
||||
const tomorrow = new Date(now);
|
||||
tomorrow.setDate(now.getDate() + 1);
|
||||
@@ -377,14 +425,14 @@ export default function ClassiquesDepartsQuais() {
|
||||
const stops = getStops(h);
|
||||
let stopMatch = null;
|
||||
|
||||
if (normalizedTarget) {
|
||||
const destNameNorm = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || '');
|
||||
if (destNameNorm && destNameNorm === normalizedTarget) {
|
||||
if (gareDisplay) {
|
||||
const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString();
|
||||
if (matchStationNameStrict(destName, gareDisplay)) {
|
||||
// Destination finale -> terminus ici, pas un départ
|
||||
continue;
|
||||
}
|
||||
const departName = normalizeName(h.gare_depart_nom || h.gare_depart || '');
|
||||
if (departName && departName === normalizedTarget) {
|
||||
const departName = (h.gare_depart_nom || h.gare_depart || '').toString();
|
||||
if (matchStationNameStrict(departName, gareDisplay)) {
|
||||
stopMatch = {
|
||||
heure_depart: h.heure_depart || h.time || h.heure,
|
||||
gare_nom: h.gare_depart_nom || h.gare_depart,
|
||||
@@ -393,8 +441,8 @@ export default function ClassiquesDepartsQuais() {
|
||||
}
|
||||
if (!stopMatch && Array.isArray(stops)) {
|
||||
for (const s of stops) {
|
||||
const sName = normalizeName(typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''));
|
||||
if (sName === normalizedTarget) {
|
||||
const sName = (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || '')).toString();
|
||||
if (matchStationNameStrict(sName, gareDisplay)) {
|
||||
stopMatch = s;
|
||||
break;
|
||||
}
|
||||
@@ -410,7 +458,11 @@ export default function ClassiquesDepartsQuais() {
|
||||
|
||||
if (!stopMatch) continue;
|
||||
|
||||
const platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || '';
|
||||
const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_depart || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_depart || h.time || h.heure;
|
||||
const depHM = parseTimeHM(rawTime);
|
||||
if (depHM == null) continue;
|
||||
|
||||
let platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || '';
|
||||
const platClean = cleanPlatform(platform);
|
||||
|
||||
if (targetQuaiClean) {
|
||||
@@ -419,10 +471,6 @@ export default function ClassiquesDepartsQuais() {
|
||||
}
|
||||
}
|
||||
|
||||
const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_depart || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_depart || h.time || h.heure;
|
||||
const depHM = parseTimeHM(rawTime);
|
||||
if (depHM == null) continue;
|
||||
|
||||
candidates.push({
|
||||
horaire: h,
|
||||
stop: stopMatch,
|
||||
@@ -450,7 +498,16 @@ export default function ClassiquesDepartsQuais() {
|
||||
: (filteredCandidates.length > 0 ? filteredCandidates.sort((a, b) => a.depHM - b.depHM)[0] : null));
|
||||
|
||||
if (chosen) {
|
||||
chosenTrain = chosen;
|
||||
const partnerCand = filteredCandidates.find(c => {
|
||||
if (!c || c === chosen) return false;
|
||||
const cNum = (c.horaire.numero_train || c.horaire.num || '').toString();
|
||||
const chosenNum = (chosen.horaire.numero_train || chosen.horaire.num || '').toString();
|
||||
return cNum !== chosenNum && c.depHM === chosen.depHM;
|
||||
});
|
||||
chosenTrain = {
|
||||
...chosen,
|
||||
partner: partnerCand ? partnerCand.horaire : null
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -503,8 +560,7 @@ export default function ClassiquesDepartsQuais() {
|
||||
|
||||
// Filtrer les arrêts à partir de la gare courante
|
||||
if (gareDisplay && stopsList.length > 0) {
|
||||
const normCur = normalizeName(gareDisplay);
|
||||
const foundIdx = stopsList.findIndex(s => normalizeName(s) === normCur);
|
||||
const foundIdx = stopsList.findIndex(s => matchStationNameStrict(s, gareDisplay));
|
||||
if (foundIdx >= 0 && foundIdx < stopsList.length - 1) {
|
||||
stopsList = stopsList.slice(foundIdx + 1);
|
||||
}
|
||||
@@ -512,9 +568,8 @@ export default function ClassiquesDepartsQuais() {
|
||||
|
||||
// Garantir que le terminus est présent en dernier
|
||||
if (destinationName) {
|
||||
const destNorm = normalizeName(destinationName);
|
||||
const lastNorm = stopsList.length > 0 ? normalizeName(stopsList[stopsList.length - 1]) : '';
|
||||
if (lastNorm !== destNorm) {
|
||||
const lastStop = stopsList.length > 0 ? stopsList[stopsList.length - 1] : '';
|
||||
if (!matchStationNameStrict(lastStop, destinationName)) {
|
||||
stopsList.push(destinationName);
|
||||
}
|
||||
}
|
||||
@@ -567,10 +622,64 @@ export default function ClassiquesDepartsQuais() {
|
||||
}
|
||||
}
|
||||
|
||||
const partner = selectedHoraire?.partner;
|
||||
const destinationName1 = destinationName;
|
||||
const destinationName2 = partner ? (partner.gare_arrivee_nom || partner.gare_arrivee || partner.destination || partner.dest || '') : (horaire?.second_destination || horaire?.dest2 || horaire?.destination2 || (horaire?.tranches && horaire?.tranches[1]?.destination) || '');
|
||||
|
||||
const trainType1 = trainType;
|
||||
const trainType2 = partner ? (partner.type_train || partner.train_type || partner.type || partner.op || 'TGV') : (horaire?.type_train2 || horaire?.train_type2 || (destinationName2 ? trainType : ''));
|
||||
|
||||
const trainNumber1 = trainNumber;
|
||||
const trainNumber2 = partner ? (partner.numero_train || partner.numero || partner.num || '') : (horaire?.numero_train2 || horaire?.num2 || (horaire?.tranches && horaire?.tranches[1]?.num) || '');
|
||||
|
||||
const isDifferentDest = Boolean(destinationName1 && destinationName2 && !matchStationNameStrict(destinationName1, destinationName2));
|
||||
|
||||
// Récupération des arrêts de la deuxième tranche
|
||||
let stopsList2 = [];
|
||||
if (partner) {
|
||||
const rawStops2 = getStops(partner);
|
||||
if (Array.isArray(rawStops2) && rawStops2.length > 0) {
|
||||
stopsList2 = rawStops2.map(s => (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''))).filter(Boolean);
|
||||
}
|
||||
if (gareDisplay && stopsList2.length > 0) {
|
||||
const fIdx = stopsList2.findIndex(s => matchStationNameStrict(s, gareDisplay));
|
||||
if (fIdx >= 0 && fIdx < stopsList2.length - 1) {
|
||||
stopsList2 = stopsList2.slice(fIdx + 1);
|
||||
}
|
||||
}
|
||||
if (destinationName2 && !matchStationNameStrict(stopsList2[stopsList2.length - 1] || '', destinationName2)) {
|
||||
stopsList2.push(destinationName2);
|
||||
}
|
||||
}
|
||||
|
||||
// Calcul du tronc commun et des branches de séparation (Fourche Image 2)
|
||||
let commonStops = [];
|
||||
let branch1Stops = [];
|
||||
let branch2Stops = [];
|
||||
|
||||
if (isDifferentDest && stopsList.length > 0 && stopsList2.length > 0) {
|
||||
let divIdx = 0;
|
||||
const minLen = Math.min(stopsList.length, stopsList2.length);
|
||||
while (divIdx < minLen && matchStationNameStrict(stopsList[divIdx], stopsList2[divIdx])) {
|
||||
commonStops.push(stopsList[divIdx]);
|
||||
divIdx++;
|
||||
}
|
||||
branch1Stops = stopsList.slice(divIdx);
|
||||
branch2Stops = stopsList2.slice(divIdx);
|
||||
}
|
||||
|
||||
const isTgv = Boolean(
|
||||
(trainType1 || '').toUpperCase().includes('TGV') ||
|
||||
(trainType2 || '').toUpperCase().includes('TGV') ||
|
||||
(operator || '').toUpperCase().includes('TGV') ||
|
||||
(brand && (brand.label || '').toUpperCase().includes('TGV')) ||
|
||||
(horaire && ((horaire.type_train || horaire.type || '').toUpperCase().includes('TGV') || (horaire.op || '').toUpperCase().includes('TGV')))
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="aff-classique-quai-dep__container">
|
||||
{/* PANNEAU GAUCHE : BLANC (Conforme Image 1) */}
|
||||
<div className="aff-classique-quai-dep__left">
|
||||
{/* PANNEAU GAUCHE : BLANC (Conforme Image 1 & Image 2) */}
|
||||
<div className={`aff-classique-quai-dep__left ${isTgv ? 'aff-classique-quai-dep__left--tgv' : ''}`}>
|
||||
{/* Logo Couleur Dynamique */}
|
||||
<div className="aff-classique-quai-dep__brand">
|
||||
<img src={brand.logo} alt={brand.label} className="aff-classique-quai-dep__brand-logo" />
|
||||
@@ -581,35 +690,120 @@ export default function ClassiquesDepartsQuais() {
|
||||
|
||||
{/* Heure et Statut */}
|
||||
<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' : ''}`}>
|
||||
{formattedTime || '--:--'}
|
||||
</span>
|
||||
<span className={`aff-classique-quai-dep__status ${isSuppressed ? 'aff-classique-quai-dep__status--suppressed' : ''} ${isDelayed ? 'aff-classique-quai-dep__status--delayed' : ''}`}>
|
||||
{(isSuppressed || isDelayed) ? (
|
||||
showStatusAlt ? (
|
||||
<span className={`aff-classique-quai-dep__status-alt ${isSuppressed ? 'aff-classique-quai-dep__status--suppressed' : 'aff-classique-quai-dep__status--delayed'}`}>
|
||||
{displayStatusText}
|
||||
</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>
|
||||
|
||||
{/* Destination Principale */}
|
||||
<div className="aff-classique-quai-dep__destination">
|
||||
{destinationName || 'Aucun départ prévu'}
|
||||
<div className={`aff-classique-quai-dep__destination ${isDifferentDest ? 'aff-classique-quai-dep__destination--multi' : ''}`}>
|
||||
{destinationName1 || 'Aucun départ prévu'}
|
||||
</div>
|
||||
|
||||
{/* Type de Train & Numéro */}
|
||||
{/* Type de Train & Numéro 1 */}
|
||||
<div className="aff-classique-quai-dep__train-info">
|
||||
{trainType && <span className="aff-classique-quai-dep__train-type">{trainType}</span>}
|
||||
{trainNumber && <span className="aff-classique-quai-dep__train-number">{trainNumber}</span>}
|
||||
{trainType1 && <span className="aff-classique-quai-dep__train-type">{trainType1}</span>}
|
||||
{trainNumber1 && <span className="aff-classique-quai-dep__train-number">{trainNumber1}</span>}
|
||||
</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) */}
|
||||
<div className="aff-classique-quai-dep__watermark" aria-hidden="true">
|
||||
<span>départ</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PANNEAU DROIT : BLEU NUIT AVEC DESSERTE (Conforme Image 1) */}
|
||||
<div className="aff-classique-quai-dep__right">
|
||||
{/* PANNEAU DROIT : BLEU NUIT AVEC DESSERTE (Conforme Image 1 & Fourche Image 2) */}
|
||||
<div className={`aff-classique-quai-dep__right ${isTgv ? 'aff-classique-quai-dep__right--tgv' : ''}`}>
|
||||
<div className="aff-classique-quai-dep__stops-wrapper">
|
||||
{stopsList.length > 0 ? (
|
||||
{isDifferentDest && commonStops.length > 0 ? (
|
||||
/* AFFICHAGE FOURCHE / BIFURCATION (Conforme Image 2) */
|
||||
<div className="aff-classique-quai-dep__fork-container">
|
||||
{/* Tronc commun */}
|
||||
<ul className="aff-classique-quai-dep__stops-list">
|
||||
{commonStops.map((stopName, idx) => (
|
||||
<li key={idx} className="aff-classique-quai-dep__stop-item">
|
||||
<span className="aff-classique-quai-dep__bullet" />
|
||||
<span className="aff-classique-quai-dep__stop-name">{stopName}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Ligne d'embranchement avec courbe */}
|
||||
<div className="aff-classique-quai-dep__fork-split-row">
|
||||
<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>
|
||||
) : (
|
||||
/* 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;
|
||||
@@ -630,19 +824,30 @@ export default function ClassiquesDepartsQuais() {
|
||||
<div style={{ color: '#ffffff', fontSize: '38px', fontWeight: 600, padding: '40px' }}>
|
||||
Veuillez consulter les écrans du hall de la gare.
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BANDEAU BAS : PLEINE LARGEUR & HORLOGE (Conforme Image 1) */}
|
||||
{/* BANDEAU BAS : TGV COMPOSITION OU BANDEAU STANDARD */}
|
||||
{isTgv ? (
|
||||
<TgvCompositionQuai
|
||||
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 ? `Voie ${cleanPlatform(horaire.quai).toUpperCase()}` : '')}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@@ -24569,6 +24569,7 @@ function cleanStationQuery(s) {
|
||||
const STATION_ALIASES = {
|
||||
'valence tgv': '87763029',
|
||||
'valence': '87761007',
|
||||
'valence ville': '87761007',
|
||||
'paris nord': '87271007',
|
||||
'paris gare du nord': '87271007',
|
||||
'paris est': '87113001',
|
||||
@@ -24581,11 +24582,18 @@ const STATION_ALIASES = {
|
||||
'austerlitz': '87547000',
|
||||
'paris bercy': '87686667',
|
||||
'bercy': '87686667',
|
||||
'paris saint lazare': '87384008',
|
||||
'saint lazare': '87384008',
|
||||
'marseille': '87751008',
|
||||
'marseille saint charles': '87751008',
|
||||
'marseille st charles': '87751008',
|
||||
'marseille blancarde': '87751081',
|
||||
'lyon part dieu': '87723197',
|
||||
'lyon perrache': '87722025',
|
||||
'lyon jean mace': '87282624',
|
||||
'lyon gorge de loup': '87721175',
|
||||
'lyon vaise': '87721001',
|
||||
'lyon saint paul': '87721159',
|
||||
'lyon saint exupery': '87762906',
|
||||
'lyon st exupery': '87762906',
|
||||
'toulouse': '87611004',
|
||||
@@ -24614,9 +24622,73 @@ const STATION_ALIASES = {
|
||||
'besancon franche comte tgv': '87300863',
|
||||
'dijon': '87713040',
|
||||
'dijon ville': '87713040',
|
||||
'dijon centre': '87713040',
|
||||
'dijon porte neuve': '87713073',
|
||||
'dijon porte-neuve': '87713073',
|
||||
'porte neuve': '87713073',
|
||||
'cannes': '87757625',
|
||||
'cannes la bocca': '87757617',
|
||||
'nice': '87756056',
|
||||
'nice ville': '87756056',
|
||||
'nice riquier': '87756353',
|
||||
'nice saint augustin': '87756254',
|
||||
'toulon': '87755009',
|
||||
'toulon sainte musse': '87742262',
|
||||
'amiens': '87313874'
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalise un nom de gare pour comparaison stricte
|
||||
*/
|
||||
export function normalizeStation(name) {
|
||||
if (!name) return '';
|
||||
return String(name)
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/^gare\s+d['e]\s+/i, '')
|
||||
.replace(/[-_/.,()'&]+/g, ' ')
|
||||
.replace(/\bst\b/g, 'saint')
|
||||
.replace(/\bste\b/g, 'sainte')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare strictement deux noms de gares pour éviter la confusion entre
|
||||
* gares de même ville (ex: "Dijon" vs "Dijon Porte Neuve", "Lyon Part Dieu" vs "Lyon Perrache")
|
||||
*/
|
||||
export function matchStationNameStrict(a, b) {
|
||||
if (!a || !b) return false;
|
||||
const normA = normalizeStation(a);
|
||||
const normB = normalizeStation(b);
|
||||
if (!normA || !normB) return false;
|
||||
if (normA === normB) return true;
|
||||
|
||||
// Liste des qualificatifs de sous-gares / gares secondaires ou périphériques distinctes
|
||||
const distinctQualifiers = [
|
||||
'porte neuve', 'part dieu', 'perrache', 'jean mace', 'gorge de loup', 'vaise',
|
||||
'saint paul', 'saint augustin', 'riquier', 'la bocca', 'bocca', 'pont du gard',
|
||||
'sud de france', 'sainte musse', 'europe', 'flandres', 'chateaucreux', 'bellevue',
|
||||
'carnot', 'terrasse', 'estressin', 'roche', 'franche comte tgv', 'montbeliard tgv'
|
||||
];
|
||||
|
||||
// Si l'une contient un qualificatif distinct et pas l'autre -> Ne correspondent PAS
|
||||
for (const q of distinctQualifiers) {
|
||||
const aHas = normA.includes(q);
|
||||
const bHas = normB.includes(q);
|
||||
if (aHas !== bHas) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Si on retire "ville" ou "centre", vérifions si elles sont identiques
|
||||
const stripGeneric = (s) => s.replace(/\b(ville|centre|gare)\b/g, '').replace(/\s+/g, ' ').trim();
|
||||
const sA = stripGeneric(normA);
|
||||
const sB = stripGeneric(normB);
|
||||
return sA === sB;
|
||||
}
|
||||
|
||||
export function findGtfsGare(query) {
|
||||
if (!query) return null;
|
||||
const rawQ = String(query).trim();
|
||||
@@ -24633,7 +24705,11 @@ export function findGtfsGare(query) {
|
||||
if (found) return found;
|
||||
}
|
||||
|
||||
// 3. Algorithme de scoring par tokens et similarité
|
||||
// 3. Recherche par correspondance exacte nette
|
||||
const exact = GTFS_GARES.find(g => cleanStationQuery(g.nom) === qClean);
|
||||
if (exact) return exact;
|
||||
|
||||
// 4. Algorithme de scoring par tokens et similarité
|
||||
const qTokens = qClean.split(' ').filter(Boolean);
|
||||
const wantsTgv = qTokens.includes('tgv');
|
||||
|
||||
@@ -24649,8 +24725,12 @@ export function findGtfsGare(query) {
|
||||
|
||||
if (gClean === qClean) {
|
||||
score = 1000;
|
||||
} else if (gClean.startsWith(qClean)) {
|
||||
score = 500;
|
||||
} else if (matchStationNameStrict(g.nom, rawQ)) {
|
||||
score = 900;
|
||||
} else if (gClean.startsWith(qClean + ' ')) {
|
||||
// Sous-gare ayant le préfixe mais plus longue (ex: "dijon porte neuve" quand la requête est "dijon")
|
||||
// Pénaliser si l'utilisateur a tapé une requête courte exacte
|
||||
score = 400 - (gTokens.length - qTokens.length) * 50;
|
||||
} else {
|
||||
let allFound = true;
|
||||
let tokenMatches = 0;
|
||||
@@ -24665,15 +24745,15 @@ export function findGtfsGare(query) {
|
||||
}
|
||||
|
||||
if (allFound) {
|
||||
score = 200 + tokenMatches * 20 - (gTokens.length - qTokens.length) * 5;
|
||||
score = 300 + tokenMatches * 20 - (gTokens.length - qTokens.length) * 40;
|
||||
} else if (tokenMatches > 0) {
|
||||
score = tokenMatches * 15 - (gTokens.length - tokenMatches) * 5;
|
||||
score = tokenMatches * 15 - (gTokens.length - tokenMatches) * 30;
|
||||
}
|
||||
}
|
||||
|
||||
if (wantsTgv && hasTgv) score += 150;
|
||||
if (wantsTgv && !hasTgv) score -= 200;
|
||||
if (!wantsTgv && hasTgv) score -= 50;
|
||||
if (!wantsTgv && hasTgv) score -= 100;
|
||||
|
||||
if (!wantsTgv && gClean.includes('ville') && qTokens.length === 1 && gTokens[0] === qTokens[0]) {
|
||||
score += 100;
|
||||
@@ -24707,6 +24787,8 @@ export function searchGtfsGares(query, limit = 20) {
|
||||
|
||||
if (gClean === qClean) {
|
||||
scored.push({ gare: g, score: 1000 });
|
||||
} else if (matchStationNameStrict(g.nom, query)) {
|
||||
scored.push({ gare: g, score: 950 });
|
||||
} else if (gClean.startsWith(qClean)) {
|
||||
scored.push({ gare: g, score: 800 });
|
||||
} else {
|
||||
@@ -24718,9 +24800,9 @@ export function searchGtfsGares(query, limit = 20) {
|
||||
}
|
||||
}
|
||||
if (matchCount === qTokens.length) {
|
||||
scored.push({ gare: g, score: 500 + matchCount * 10 - gTokens.length });
|
||||
scored.push({ gare: g, score: 500 + matchCount * 10 - gTokens.length * 10 });
|
||||
} else if (matchCount > 0) {
|
||||
scored.push({ gare: g, score: matchCount * 20 - gTokens.length });
|
||||
scored.push({ gare: g, score: matchCount * 20 - gTokens.length * 10 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+538
-77
@@ -5,23 +5,81 @@ import { findGtfsGare, searchGtfsGares } from './gtfsStations';
|
||||
const SNCF_GTFS_RT_TRIP_UPDATES = 'https://proxy.transport.data.gouv.fr/resource/sncf-gtfs-rt-trip-updates';
|
||||
const SNCF_GTFS_RT_SERVICE_ALERTS = 'https://proxy.transport.data.gouv.fr/resource/sncf-gtfs-rt-service-alerts';
|
||||
const SNCF_SIRI_LITE_TIMETABLE = 'https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-estimated-timetable';
|
||||
const SNCF_SIRI_LITE_SITUATION_EXCHANGE = 'https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-situation-exchange';
|
||||
const NAVITIA_BASE_URL = 'https://api.sncf.com/v1';
|
||||
const NAVITIA_TOKEN = (typeof process !== 'undefined' && process.env && process.env.REACT_APP_NAVITIA_TOKEN) || 'bf06df97-2ff1-4560-b74c-f127cb70764d';
|
||||
|
||||
// Cache mémoire pour le flux GTFS-RT (TTL = 30s)
|
||||
// TTL de rafraîchissement des flux temps réel SNCF : 2 minutes (120 secondes)
|
||||
const REALTIME_CACHE_TTL = 120000;
|
||||
|
||||
// Cache mémoire pour le flux GTFS-RT (TTL = 2 min)
|
||||
let gtfsRtCache = {
|
||||
timestamp: 0,
|
||||
updates: new Map(),
|
||||
alerts: []
|
||||
};
|
||||
|
||||
// Cache mémoire pour le flux SIRI-Lite (TTL = 30s)
|
||||
// Cache mémoire pour le flux SIRI-Lite Estimated Timetable (TTL = 2 min)
|
||||
let siriLiteCache = {
|
||||
timestamp: 0,
|
||||
platforms: new Map(),
|
||||
updates: new Map()
|
||||
};
|
||||
|
||||
// Cache mémoire pour le flux SIRI-Lite Situation Exchange (TTL = 2 min)
|
||||
let situationExchangeCache = {
|
||||
timestamp: 0,
|
||||
platforms: new Map(),
|
||||
updates: new Map(),
|
||||
alerts: []
|
||||
};
|
||||
|
||||
// Cache mémoire pour l'API OpenData SNCF (TTL = 2 min)
|
||||
const openDataPlatformsCache = new Map();
|
||||
|
||||
const MAJOR_HUBS = [
|
||||
'paris', 'lyon', 'dijon', 'marseille', 'lille', 'strasbourg', 'bordeaux',
|
||||
'toulouse', 'nantes', 'rennes', 'nice', 'montpellier', 'nimes', 'metz',
|
||||
'nancy', 'mulhouse', 'tours', 'le mans', 'rouen', 'clermont ferrand',
|
||||
'grenoble', 'chambery', 'avignon', 'valence', 'besancon viotte',
|
||||
'saint etienne', 'toulon', 'cannes', 'antibes', 'angers'
|
||||
];
|
||||
|
||||
const MAJOR_HUB_UICS = [
|
||||
'87713040', '87686006', '87271007', '87391003', '87113001', '87384008',
|
||||
'87547000', '87686667', '87722025', '87723197', '87751008', '87286005',
|
||||
'87286013', '87212027', '87581009', '87611004', '87481002', '87471003',
|
||||
'87756056', '87773002', '87775007', '87192039', '87141002', '87182063',
|
||||
'87571000', '87444000', '87411017', '87734004', '87747006', '87741009',
|
||||
'87765008', '87318964', '87763029', '87718007', '87726000', '87755009',
|
||||
'87757625', '87757674', '87484006'
|
||||
];
|
||||
|
||||
export function isMajorHub(stationName, uic = '') {
|
||||
if (uic && MAJOR_HUB_UICS.includes(String(uic).replace(/\D/g, ''))) return true;
|
||||
if (!stationName) return false;
|
||||
const clean = String(stationName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
return MAJOR_HUBS.some(hub => clean === hub || clean.startsWith(hub + ' ') || clean.endsWith(' ' + hub));
|
||||
}
|
||||
|
||||
export function isCarNumber(num) {
|
||||
const d = String(num || '').replace(/\D/g, '');
|
||||
return (d.length === 5 && d.startsWith('3')) || (d.length === 6 && d.startsWith('3'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Résout la voie pour une circulation :
|
||||
* - Aucune voie nominale/fictive inventée : seuls les quais temps réel SNCF sont affichés.
|
||||
* - Les services par autocar affichent 'AUTOCAR' (pictogramme).
|
||||
*/
|
||||
export function resolveNominalTrack(stationName, trainNum, uic = '') {
|
||||
const rawNum = String(trainNum || '').replace(/\D/g, '');
|
||||
if (isCarNumber(rawNum)) return 'AUTOCAR';
|
||||
return '';
|
||||
}
|
||||
|
||||
// Helper fetch avec timeout pour ne jamais bloquer l'affichage en cas de réseau lent ou de blocage
|
||||
async function fetchWithTimeout(url, options = {}, timeoutMs = 7000) {
|
||||
async function fetchWithTimeout(url, options = {}, timeoutMs = 25000) {
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
@@ -40,12 +98,53 @@ function cleanNum(n) {
|
||||
return m ? m[0].replace(/^0+/, '') : String(n).trim();
|
||||
}
|
||||
|
||||
function extractAllTrainNumbers(journeyXml) {
|
||||
const nums = new Set();
|
||||
|
||||
// 1. TrainNumberRef
|
||||
const tnr = journeyXml.match(/<TrainNumberRef>([^<]+)<\/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() {
|
||||
const now = Date.now();
|
||||
if (now - siriLiteCache.timestamp < 30000 && siriLiteCache.platforms.size > 0) {
|
||||
if (now - siriLiteCache.timestamp < REALTIME_CACHE_TTL && siriLiteCache.platforms.size > 0) {
|
||||
return siriLiteCache;
|
||||
}
|
||||
|
||||
@@ -53,79 +152,112 @@ async function fetchSiriLiteData() {
|
||||
const updatesMap = new Map();
|
||||
|
||||
try {
|
||||
const res = await fetchWithTimeout(SNCF_SIRI_LITE_TIMETABLE, {}, 8000);
|
||||
const res = await fetchWithTimeout(SNCF_SIRI_LITE_TIMETABLE, {
|
||||
headers: {
|
||||
'Accept': 'application/xml, text/xml, */*'
|
||||
}
|
||||
}, 25000);
|
||||
|
||||
if (res && res.ok) {
|
||||
const xml = await res.text();
|
||||
const journeys = xml.split('<EstimatedVehicleJourney>');
|
||||
|
||||
for (let i = 1; i < journeys.length; 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 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)
|
||||
const callMatches = j.match(/<(RecordedCall|EstimatedCall)>[\s\S]*?<\/\1>/g) || [];
|
||||
// 2. Extraire tous les arrêts desservis (RecordedCall et EstimatedCall)
|
||||
const callMatches = j.match(/<(?:RecordedCall|EstimatedCall)>[\s\S]*?<\/(?:RecordedCall|EstimatedCall)>/g) || [];
|
||||
for (const call of callMatches) {
|
||||
const stopRefMatch = call.match(/<StopPointRef>[^:]*::(\d+)<\/StopPointRef>/) || call.match(/<StopPointRef>([^<]+)<\/StopPointRef>/);
|
||||
const uicMatch = stopRefMatch ? (stopRefMatch[1].match(/\d{7,8}/) || [stopRefMatch[1]]) : null;
|
||||
const uic = uicMatch ? uicMatch[0] : '';
|
||||
// Extraction du code UIC de l'arrêt
|
||||
const stopRefMatch = call.match(/<StopPointRef>([^<]+)<\/StopPointRef>/);
|
||||
let uic = '';
|
||||
let uic7 = '';
|
||||
if (stopRefMatch) {
|
||||
const rawRef = stopRefMatch[1];
|
||||
const uicDigits = rawRef.match(/\d{7,8}/);
|
||||
uic = uicDigits ? uicDigits[0] : rawRef.replace(/\D/g, '');
|
||||
if (uic.length === 8 && uic.startsWith('87')) {
|
||||
uic7 = uic.slice(2);
|
||||
}
|
||||
}
|
||||
|
||||
const stopNameMatch = call.match(/<StopPointName>([^<]+)<\/StopPointName>/);
|
||||
const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
const depPlatMatch = call.match(/<DeparturePlatformName>([^<]+)<\/DeparturePlatformName>/);
|
||||
const arrPlatMatch = call.match(/<ArrivalPlatformName>([^<]+)<\/ArrivalPlatformName>/);
|
||||
const platform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : '')).trim();
|
||||
// Extraction des quais (Départ, Arrivée, Prévu ou Réel)
|
||||
const depPlatMatch = call.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName)>([^<]+)<\/(?:DeparturePlatformName|PlannedDeparturePlatformName)>/);
|
||||
const arrPlatMatch = call.match(/<(?:ArrivalPlatformName|PlannedArrivalPlatformName)>([^<]+)<\/(?:ArrivalPlatformName|PlannedArrivalPlatformName)>/);
|
||||
const quayMatch = call.match(/<QuayRef>([^<]+)<\/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 aimDepMatch = call.match(/<AimedDepartureTime>([^<]+)<\/AimedDepartureTime>/);
|
||||
const expArrMatch = call.match(/<ExpectedArrivalTime>([^<]+)<\/ExpectedArrivalTime>/);
|
||||
const aimArrMatch = call.match(/<AimedArrivalTime>([^<]+)<\/AimedArrivalTime>/);
|
||||
|
||||
let delayMin = 0;
|
||||
if (expDepMatch && aimDepMatch) {
|
||||
const expD = new Date(expDepMatch[1]).getTime();
|
||||
const aimD = new Date(aimDepMatch[1]).getTime();
|
||||
const expTimeStr = (expDepMatch && expDepMatch[1]) || (expArrMatch && expArrMatch[1]);
|
||||
const aimTimeStr = (aimDepMatch && aimDepMatch[1]) || (aimArrMatch && aimArrMatch[1]);
|
||||
|
||||
if (expTimeStr && aimTimeStr) {
|
||||
const expD = new Date(expTimeStr).getTime();
|
||||
const aimD = new Date(aimTimeStr).getTime();
|
||||
if (!isNaN(expD) && !isNaN(aimD)) {
|
||||
delayMin = Math.max(0, Math.round((expD - aimD) / 60000));
|
||||
}
|
||||
}
|
||||
|
||||
const isStopCancelled = isCanceledJourney || call.includes('<DepartureStatus>cancelled</DepartureStatus>') || call.includes('<ArrivalStatus>cancelled</ArrivalStatus>') || call.includes('<Cancellation>true</Cancellation>');
|
||||
|
||||
if (platform) {
|
||||
for (const tNum of trainNums) {
|
||||
if (uic) {
|
||||
if (trainNum) {
|
||||
platformsMap.set(`${trainNum}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${trainNum}`, platform);
|
||||
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);
|
||||
}
|
||||
if (rawTrainNum) {
|
||||
platformsMap.set(`${rawTrainNum}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${rawTrainNum}`, platform);
|
||||
}
|
||||
if (journeyRef) {
|
||||
if (uic) {
|
||||
platformsMap.set(`${journeyRef}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${journeyRef}`, platform);
|
||||
}
|
||||
}
|
||||
if (stopNameClean) {
|
||||
if (trainNum) platformsMap.set(`${trainNum}_${stopNameClean}`, platform);
|
||||
if (rawTrainNum) platformsMap.set(`${rawTrainNum}_${stopNameClean}`, platform);
|
||||
if (journeyRef) platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
|
||||
platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (trainNum || rawTrainNum || journeyRef) {
|
||||
const info = {
|
||||
platform: platform,
|
||||
delayMinutes: delayMin,
|
||||
isCanceled: isCanceled
|
||||
isCanceled: isStopCancelled
|
||||
};
|
||||
if (trainNum) updatesMap.set(trainNum, info);
|
||||
if (rawTrainNum) updatesMap.set(rawTrainNum, info);
|
||||
if (journeyRef) updatesMap.set(journeyRef, info);
|
||||
|
||||
for (const tNum of trainNums) {
|
||||
if (uic) updatesMap.set(`${tNum}_${uic}`, info);
|
||||
updatesMap.set(tNum, info);
|
||||
}
|
||||
if (journeyRef) {
|
||||
if (uic) updatesMap.set(`${journeyRef}_${uic}`, info);
|
||||
updatesMap.set(journeyRef, info);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,12 +269,298 @@ async function fetchSiriLiteData() {
|
||||
updates: updatesMap
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite refresh error/timeout:', err);
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Estimated Timetable refresh error/timeout:', err);
|
||||
}
|
||||
|
||||
return siriLiteCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère et décode le flux SIRI Lite Situation Exchange SNCF (Circulations à venir, perturbations, changements de voie, suppressions)
|
||||
* Source : https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-situation-exchange
|
||||
*/
|
||||
async function fetchSituationExchangeData() {
|
||||
const now = Date.now();
|
||||
if (now - situationExchangeCache.timestamp < REALTIME_CACHE_TTL && situationExchangeCache.platforms.size > 0) {
|
||||
return situationExchangeCache;
|
||||
}
|
||||
|
||||
const platformsMap = new Map();
|
||||
const updatesMap = new Map();
|
||||
const alertsList = [];
|
||||
|
||||
try {
|
||||
const res = await fetchWithTimeout(SNCF_SIRI_LITE_SITUATION_EXCHANGE, {
|
||||
headers: {
|
||||
'Accept': 'application/xml, text/xml, */*'
|
||||
}
|
||||
}, 12000);
|
||||
|
||||
if (res && res.ok) {
|
||||
const xml = await res.text();
|
||||
const situations = xml.split('<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
|
||||
*/
|
||||
@@ -311,8 +729,8 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
const publicUrl = (typeof process !== 'undefined' && process.env && process.env.PUBLIC_URL) ? process.env.PUBLIC_URL : '';
|
||||
const stationUrl = `${publicUrl}/data/stations/${uic}.json`;
|
||||
|
||||
// Chargement parallèle tolérant aux pannes réseau / CORS
|
||||
const [jsonRes, rtData, siriData] = await Promise.all([
|
||||
// Chargement parallèle tolérant aux pannes réseau / CORS (OpenData SNCF, SIRI-Lite Estimated Timetable, SIRI-Lite Situation Exchange, GTFS-RT)
|
||||
const [jsonRes, rtData, siriData, sxData, openDataPlatforms] = await Promise.all([
|
||||
fetch(stationUrl).catch(err => {
|
||||
console.warn(`[sncfApi] Notice: fetch station ${uic} error:`, err);
|
||||
return null;
|
||||
@@ -322,8 +740,16 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
return { updates: new Map(), alerts: [] };
|
||||
}),
|
||||
fetchSiriLiteData().catch(err => {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite error:', err);
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Timetable error:', err);
|
||||
return { platforms: new Map(), updates: new Map() };
|
||||
}),
|
||||
fetchSituationExchangeData().catch(err => {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Situation Exchange error:', err);
|
||||
return { platforms: new Map(), updates: new Map(), alerts: [] };
|
||||
}),
|
||||
fetchOpenDataSncfPlatforms(uic, mode).catch(err => {
|
||||
console.warn('[sncfApi] Notice: OpenData SNCF platform error:', err);
|
||||
return new Map();
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -333,6 +759,8 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
}
|
||||
|
||||
const data = await jsonRes.json();
|
||||
const gareRef = findGtfsGare(uic);
|
||||
const targetStationNom = (gareRef && gareRef.nom) || (data && data.nom) || '';
|
||||
const rawList = mode === 'arrivees' ? data.arrivees : data.departs;
|
||||
if (!Array.isArray(rawList) || rawList.length === 0) return null;
|
||||
|
||||
@@ -362,21 +790,18 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
if (Array.isArray(customDaysRaw)) {
|
||||
customDays = customDaysRaw;
|
||||
} else if (typeof customDaysRaw === 'string') {
|
||||
try { customDays = JSON.parse(customDaysRaw); } catch (e) {}
|
||||
}
|
||||
if (Array.isArray(customDays) && customDays.length > 0) {
|
||||
const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetYYYYMMDD);
|
||||
if (match) return Boolean(match.circule);
|
||||
try { customDays = JSON.parse(customDaysRaw); } catch (_) { customDays = []; }
|
||||
}
|
||||
if (customDays.includes(targetDateHyphen)) return true;
|
||||
}
|
||||
|
||||
// 3. Colonnes circule_lundi, circule_mardi...
|
||||
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
|
||||
const colName = `circule_${dayNames[targetDay]}`;
|
||||
if (typeof t[colName] !== 'undefined' && t[colName] !== null) {
|
||||
const db = t.date_debut ? String(t.date_debut).slice(0, 10) : null;
|
||||
// 3. Base Supabase : circulation standard par jour de semaine
|
||||
const daysMap = ['circule_dimanche', 'circule_lundi', 'circule_mardi', 'circule_mercredi', 'circule_jeudi', 'circule_vendredi', 'circule_samedi'];
|
||||
const colName = daysMap[targetDay];
|
||||
if (typeof t[colName] !== 'undefined') {
|
||||
const dd = t.date_debut ? String(t.date_debut).slice(0, 10) : null;
|
||||
const df = t.date_fin ? String(t.date_fin).slice(0, 10) : null;
|
||||
if (db && targetDateHyphen < db) return false;
|
||||
if (dd && targetDateHyphen < dd) return false;
|
||||
if (df && targetDateHyphen > df) return false;
|
||||
return Boolean(t[colName]);
|
||||
}
|
||||
@@ -399,21 +824,37 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
return true;
|
||||
};
|
||||
|
||||
// Helper de résolution de quai
|
||||
const lookupPlatform = (trainNum, tripId, stationUic, stationName) => {
|
||||
// Helper de résolution de quai en temps réel (Estimated Timetable + Situation Exchange + OpenData)
|
||||
const lookupPlatform = (trainNum, tripId, stationUic, stationName, rawTimeStr = '', destName = '') => {
|
||||
const cNum = cleanNum(trainNum);
|
||||
const rawNum = String(trainNum || '').replace(/\D/g, '');
|
||||
const cName = stationName ? formatStationName(stationName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
const cDest = destName ? formatStationName(destName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
const uicClean = String(stationUic || '').replace(/\D/g, '');
|
||||
const uic7 = (uicClean.length === 8 && uicClean.startsWith('87')) ? uicClean.slice(2) : '';
|
||||
|
||||
return (
|
||||
(cNum && stationUic && siriData.platforms.get(`${cNum}_${stationUic}`)) ||
|
||||
(trainNum && stationUic && siriData.platforms.get(`${trainNum}_${stationUic}`)) ||
|
||||
(stationUic && cNum && siriData.platforms.get(`${stationUic}_${cNum}`)) ||
|
||||
(stationUic && trainNum && siriData.platforms.get(`${stationUic}_${trainNum}`)) ||
|
||||
// 1. SIRI-Lite Estimated Timetable (60 prochaines min - Priorité 1)
|
||||
(cNum && uicClean && siriData.platforms.get(`${cNum}_${uicClean}`)) ||
|
||||
(rawNum && uicClean && siriData.platforms.get(`${rawNum}_${uicClean}`)) ||
|
||||
(uicClean && cNum && siriData.platforms.get(`${uicClean}_${cNum}`)) ||
|
||||
(cNum && uic7 && siriData.platforms.get(`${cNum}_${uic7}`)) ||
|
||||
(uic7 && cNum && siriData.platforms.get(`${uic7}_${cNum}`)) ||
|
||||
(cNum && cName && siriData.platforms.get(`${cNum}_${cName}`)) ||
|
||||
(trainNum && cName && siriData.platforms.get(`${trainNum}_${cName}`)) ||
|
||||
(tripId && stationUic && siriData.platforms.get(`${tripId}_${stationUic}`)) ||
|
||||
(stationUic && tripId && siriData.platforms.get(`${stationUic}_${tripId}`)) ||
|
||||
(rawNum && cName && siriData.platforms.get(`${rawNum}_${cName}`)) ||
|
||||
(tripId && uicClean && siriData.platforms.get(`${tripId}_${uicClean}`)) ||
|
||||
(tripId && cName && siriData.platforms.get(`${tripId}_${cName}`)) ||
|
||||
// 2. SIRI-Lite Situation Exchange (Circulations à venir / Changements de quai)
|
||||
(cNum && uicClean && sxData.platforms.get(`${cNum}_${uicClean}`)) ||
|
||||
(rawNum && uicClean && sxData.platforms.get(`${rawNum}_${uicClean}`)) ||
|
||||
(uicClean && cNum && sxData.platforms.get(`${uicClean}_${cNum}`)) ||
|
||||
(cNum && cName && sxData.platforms.get(`${cNum}_${cName}`)) ||
|
||||
(rawNum && cName && sxData.platforms.get(`${rawNum}_${cName}`)) ||
|
||||
// 3. OpenData SNCF direct
|
||||
(cNum && uicClean && openDataPlatforms.get(`${cNum}_${uicClean}`)) ||
|
||||
(rawNum && uicClean && openDataPlatforms.get(`${rawNum}_${uicClean}`)) ||
|
||||
(cNum && cName && openDataPlatforms.get(`${cNum}_${cName}`)) ||
|
||||
(cNum && cDest && openDataPlatforms.get(`${cNum}_${cDest}`)) ||
|
||||
''
|
||||
);
|
||||
};
|
||||
@@ -423,17 +864,30 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
const rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr);
|
||||
const timeHM = parseTimeHM(rawTime) || 0;
|
||||
|
||||
// Recherche de mise à jour GTFS-RT ou SIRI-Lite (valide pour aujourd'hui)
|
||||
// Recherche de mise à jour GTFS-RT, SIRI-Lite Timetable ou SIRI-Lite Situation Exchange
|
||||
const rt = !isTomorrow ? (rtData.updates.get(t.trip_id) || (t.num ? rtData.updates.get(t.num) : null) || (cleanNum(t.num) ? rtData.updates.get(cleanNum(t.num)) : null)) : null;
|
||||
const siri = !isTomorrow ? (siriData.updates.get(t.num) || (cleanNum(t.num) ? siriData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? siriData.updates.get(t.trip_id) : null)) : null;
|
||||
const sx = !isTomorrow ? (sxData.updates.get(t.num) || (cleanNum(t.num) ? sxData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? sxData.updates.get(t.trip_id) : null)) : null;
|
||||
|
||||
const isCanceled = (rt && rt.isCanceled) || (siri && siri.isCanceled) || false;
|
||||
const isCanceled = (rt && rt.isCanceled) || (siri && siri.isCanceled) || (sx && sx.isCanceled) || false;
|
||||
const delayMinutes = (rt && rt.delayMinutes > 0) ? rt.delayMinutes : (siri && siri.delayMinutes > 0 ? siri.delayMinutes : 0);
|
||||
|
||||
const currentStationName = mode === 'arrivees' ? t.dest : t.orig;
|
||||
let realVoie = lookupPlatform(t.num, t.trip_id, uic, currentStationName);
|
||||
if (!realVoie && t.quai && typeof t.quai === 'string') {
|
||||
realVoie = t.quai.trim();
|
||||
const currentStationName = targetStationNom || (data && data.nom) || (mode === 'arrivees' ? t.dest : t.orig);
|
||||
const destinationStation = mode === 'arrivees' ? t.orig : t.dest;
|
||||
const isAutocar = (
|
||||
(t.type && (t.type.toUpperCase().includes('AUTOCAR') || t.type.toUpperCase().includes('CAR') || t.type.toUpperCase() === 'BUS')) ||
|
||||
(t.type_train && (t.type_train.toUpperCase().includes('AUTOCAR') || t.type_train.toUpperCase().includes('CAR') || t.type_train.toUpperCase() === 'BUS')) ||
|
||||
(t.op && (t.op.toUpperCase().includes('AUTOCAR') || t.op.toUpperCase().includes('CAR') || t.op.toUpperCase() === 'BUS')) ||
|
||||
Boolean(t.is_substitution) ||
|
||||
isCarNumber(t.num || t.numero_train || t.numero)
|
||||
);
|
||||
|
||||
let realVoie = lookupPlatform(t.num, t.trip_id, uic, currentStationName, rawTime, destinationStation);
|
||||
if (!realVoie && (t.quai || t.quai_reel || t.platform || t.voie)) {
|
||||
realVoie = String(t.quai || t.quai_reel || t.platform || t.voie).replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
}
|
||||
if (!realVoie) {
|
||||
realVoie = isAutocar ? 'AUTOCAR' : resolveNominalTrack(currentStationName, t.num, uic);
|
||||
}
|
||||
|
||||
let statusLabel = "à l'heure";
|
||||
@@ -446,14 +900,24 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
etat = 'retarde';
|
||||
}
|
||||
|
||||
let cause = '';
|
||||
if (!isTomorrow && delayMinutes > 0 && rtData.alerts.length > 0) {
|
||||
let cause = (sx && sx.cause) || '';
|
||||
if (!isTomorrow && delayMinutes > 0 && !cause) {
|
||||
if (rtData.alerts.length > 0) {
|
||||
cause = rtData.alerts[idx % rtData.alerts.length].text;
|
||||
} else if (sxData.alerts.length > 0) {
|
||||
cause = sxData.alerts[idx % sxData.alerts.length].text;
|
||||
}
|
||||
}
|
||||
|
||||
const formattedStops = (t.stops || []).map((st) => {
|
||||
const stopUic = st.uic || '';
|
||||
const stopVoie = lookupPlatform(t.num, t.trip_id, stopUic, st.nom);
|
||||
let stopVoie = lookupPlatform(t.num, t.trip_id, stopUic, st.nom, st.heure, destinationStation);
|
||||
if (!stopVoie && (st.quai || st.quai_reel || st.voie || st.platform)) {
|
||||
stopVoie = String(st.quai || st.quai_reel || st.voie || st.platform).replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
}
|
||||
if (!stopVoie) {
|
||||
stopVoie = isAutocar ? 'AUTOCAR' : resolveNominalTrack(st.nom, t.num);
|
||||
}
|
||||
|
||||
return {
|
||||
uic: stopUic,
|
||||
@@ -580,13 +1044,10 @@ export async function getSncfTimetable({ gareName, mode = 'departs', quai = null
|
||||
let finalTrains = null;
|
||||
if (uic) {
|
||||
finalTrains = await fetchGtfsStationTimetable(uic, mode);
|
||||
}
|
||||
|
||||
// 2. Si non trouvé ou gare sans fichier, chercher avec le nom
|
||||
if (!finalTrains || finalTrains.length === 0) {
|
||||
const rawGare = gare ? gare.nom : gareName;
|
||||
const match = findGtfsGare(rawGare);
|
||||
if (match && match.uic && match.uic !== uic) {
|
||||
} else {
|
||||
// Si UIC non trouvé directement, chercher par nom
|
||||
const match = findGtfsGare(gareName);
|
||||
if (match && match.uic) {
|
||||
finalTrains = await fetchGtfsStationTimetable(match.uic, mode);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user