diff --git a/public/type/data.json b/public/type/data.json index 244b66d..585eebd 100644 --- a/public/type/data.json +++ b/public/type/data.json @@ -135,121 +135,121 @@ }, { "file": "logo-rer-a.svg", - "path": "/img/type/logo-rer-a.svg", + "path": "/logo-rer-a.svg", "slug": "rer-a", "name": "RER A" }, { "file": "logo-rer-b.svg", - "path": "/img/type/logo-rer-b.svg", + "path": "/logo-rer-b.svg", "slug": "rer-b", "name": "RER B" }, { "file": "logo-rer-c.svg", - "path": "/img/type/logo-rer-c.svg", + "path": "/logo-rer-c.svg", "slug": "rer-c", "name": "RER C" }, { "file": "logo-rer-d.svg", - "path": "/img/type/logo-rer-d.svg", + "path": "/logo-rer-d.svg", "slug": "rer-d", "name": "RER D" }, { "file": "logo-rer-e.svg", - "path": "/img/type/logo-rer-e.svg", + "path": "/logo-rer-e.svg", "slug": "rer-e", "name": "RER E" }, { "file": "logo-sbb.svg", - "path": "/img/type/logo-sbb.svg", + "path": "/logo-sbb.svg", "slug": "sbb", "name": "SBB" }, { "file": "logo-sncb.svg", - "path": "/img/type/logo-sncb.svg", + "path": "/logo-sncb.svg", "slug": "sncb", "name": "SNCB" }, { "file": "logo-sncf.svg", - "path": "/img/type/logo-sncf.svg", + "path": "/logo-sncf.svg", "slug": "sncf", "name": "Sncf" }, { "file": "sncf-voyageurs-logo.png", - "path": "/img/type/sncf-voyageurs-logo.png", + "path": "/sncf-voyageurs-logo.png", "slug": "sncf-voyageurs-logo", "name": "SNCF" }, { "file": "logo-sncf1985.svg", - "path": "/img/type/logo-sncf1985.svg", + "path": "/logo-sncf1985.svg", "slug": "sncf1985", "name": "Sncf1985" }, { "file": "logo-sncf1992.svg", - "path": "/img/type/logo-sncf1992.svg", + "path": "/logo-sncf1992.svg", "slug": "sncf1992", "name": "Sncf1992" }, { "file": "logo-t-h.svg", - "path": "/img/type/logo-t-h.svg", + "path": "/logo-t-h.svg", "slug": "t-h", "name": "T H" }, { "file": "logo-t-j.svg", - "path": "/img/type/logo-t-j.svg", + "path": "/logo-t-j.svg", "slug": "t-j", "name": "T J" }, { "file": "logo-t-k.svg", - "path": "/img/type/logo-t-k.svg", + "path": "/logo-t-k.svg", "slug": "t-k", "name": "T K" }, { "file": "logo-t-l.svg", - "path": "/img/type/logo-t-l.svg", + "path": "/logo-t-l.svg", "slug": "t-l", "name": "T L" }, { "file": "logo-t-n.svg", - "path": "/img/type/logo-t-n.svg", + "path": "/logo-t-n.svg", "slug": "t-n", "name": "T N" }, { "file": "logo-t-p.svg", - "path": "/img/type/logo-t-p.svg", + "path": "/logo-t-p.svg", "slug": "t-p", "name": "T P" }, { "file": "logo-t-r.svg", - "path": "/img/type/logo-t-r.svg", + "path": "/logo-t-r.svg", "slug": "t-r", "name": "T R" }, { "file": "logo-t-u.svg", - "path": "/img/type/logo-t-u.svg", + "path": "/logo-t-u.svg", "slug": "t-u", "name": "T U" }, { "file": "logo-teoz.svg", - "path": "/img/type/logo-teoz.svg", + "path": "/logo-teoz.svg", "slug": "teoz", "name": "Teoz" }, @@ -261,121 +261,121 @@ }, { "file": "logo-ter-rhone-alpes.svg", - "path": "/img/type/logo-ter-rhone-alpes.svg", + "path": "/logo-ter-rhone-alpes.svg", "slug": "ter-rhone-alpes", "name": "TER Rhone-Alpes" }, { "file": "logo-teralsace.svg", - "path": "/img/type/logo-teralsace.svg", + "path": "/logo-teralsace.svg", "slug": "teralsace", "name": "Ter Alsace" }, { "file": "logo-teraquitaine.svg", - "path": "/img/type/logo-teraquitaine.svg", + "path": "/logo-teraquitaine.svg", "slug": "teraquitaine", "name": "Ter Aquitaine" }, { "file": "logo-terbassenormandie.svg", - "path": "/img/type/logo-terbassenormandie.svg", + "path": "/logo-terbassenormandie.svg", "slug": "terbassenormandie", "name": "Ter Basse Normandie" }, { "file": "logo-terbourgogne.svg", - "path": "/img/type/logo-terbourgogne.svg", + "path": "/logo-terbourgogne.svg", "slug": "terbourgogne", "name": "Ter Bourgogne" }, { "file": "logo-terbretagne.svg", - "path": "/img/type/logo-terbretagne.svg", + "path": "/logo-terbretagne.svg", "slug": "terbretagne", "name": "Ter Bretagne" }, { "file": "logo-tercentre.svg", - "path": "/img/type/logo-tercentre.svg", + "path": "/logo-tercentre.svg", "slug": "tercentre", "name": "Ter Centre" }, { "file": "logo-terlanguedocroussillon.svg", - "path": "/img/type/logo-terlanguedocroussillon.svg", + "path": "/logo-terlanguedocroussillon.svg", "slug": "terlanguedocroussillon", "name": "Ter Languedoc-Roussillon" }, { "file": "logo-termidipyrenees.svg", - "path": "/img/type/logo-termidipyrenees.svg", + "path": "/logo-termidipyrenees.svg", "slug": "termidipyrenees", "name": "Ter Midi-Pyrenees" }, { "file": "logo-ternordpasdecalais.svg", - "path": "/img/type/logo-ternordpasdecalais.svg", + "path": "/logo-ternordpasdecalais.svg", "slug": "ternordpasdecalais", "name": "Ter Nord-pas-de-Calais" }, { "file": "logo-terpoitoucharentes.svg", - "path": "/img/type/logo-terpoitoucharentes.svg", + "path": "/logo-terpoitoucharentes.svg", "slug": "terpoitoucharentes", "name": "Ter Poitou Charentes" }, { "file": "logo-tgv.svg", - "path": "/img/type/logo-tgv.svg", + "path": "/logo-tgv.svg", "slug": "tgv", "name": "TGV" }, { "file": "logo-inoui.svg", - "path": "/img/type/logo-inoui.svg", + "path": "/logo-inoui.svg", "slug": "inoui", "name": "TGV InOui" }, { "file": "logo-lyria.svg", - "path": "/img/type/logo-lyria.svg", + "path": "/logo-lyria.svg", "slug": "lyria", "name": "TGV Lyria" }, { "file": "logo-thalys.svg", - "path": "/img/type/logo-thalys.svg", + "path": "/logo-thalys.svg", "slug": "thalys", "name": "Thalys" }, { "file": "logo-thello.svg", - "path": "/img/type/logo-thello.svg", + "path": "/logo-thello.svg", "slug": "thello", "name": "Thello" }, { "file": "track-car.svg", - "path": "/img/type/track-car.svg", + "path": "/track-car.svg", "slug": "track-car", "name": "Car TER" }, { "file": "logo-tramtrain.svg", - "path": "/img/type/logo-tramtrain.svg", + "path": "/logo-tramtrain.svg", "slug": "tramtrain", "name": "Tramtrain" }, { "file": "logo-trenitalia.svg", - "path": "/img/type/logo-trenitalia.svg", + "path": "/logo-trenitalia.svg", "slug": "trenitalia", "name": "Trenitalia" }, { "file": "logo-zou.svg", - "path": "/img/type/logo-zou.svg", + "path": "/logo-zou.svg", "slug": "zou", "name": "Zou" } diff --git a/public/type/quais/data-color.json b/public/type/quais/data-color.json index 1134e06..b4dc54d 100644 --- a/public/type/quais/data-color.json +++ b/public/type/quais/data-color.json @@ -3,379 +3,379 @@ "logos": [ { "file": "logo-aleop-color.svg", - "path": "/type/quais/logo-aleop-color.svg", + "path": "/logo-aleop-color.svg", "slug": "aleop", "name": "Aleop" }, { "file": "logo-auvergerhonealpes-color.svg", - "path": "/type/quais/logo-auvergerhonealpes-color.svg", + "path": "/logo-auvergerhonealpes-color.svg", "slug": "auvergerhonealpes", "name": "Auvergerhonealpes" }, { "file": "logo-breizhgo-color.svg", - "path": "/type/quais/logo-breizhgo-color.svg", + "path": "/logo-breizhgo-color.svg", "slug": "breizhgo", "name": "Breizhgo" }, { "file": "logo-cfl-color.svg", - "path": "/type/quais/logo-cfl-color.svg", + "path": "/logo-cfl-color.svg", "slug": "cfl", "name": "CFL" }, { "file": "logo-db-color.svg", - "path": "/type/quais/logo-db-color.svg", + "path": "/logo-db-color.svg", "slug": "db", "name": "DB" }, { "file": "logo-eurostar-color.svg", - "path": "/type/quais/logo-eurostar-color.svg", + "path": "/logo-eurostar-color.svg", "slug": "eurostar", "name": "Eurostar" }, { "file": "logo-fluo-color.svg", - "path": "/type/quais/logo-fluo-color.svg", + "path": "/logo-fluo-color.svg", "slug": "fluo", "name": "Fluo" }, { "file": "logo-frecciarossa-color.svg", - "path": "/type/quais/logo-frecciarossa-color.svg", + "path": "/logo-frecciarossa-color.svg", "slug": "frecciarossa", "name": "Frecciarossa" }, { "file": "logo-hautsdefrance-color.svg", - "path": "/type/quais/logo-hautsdefrance-color.svg", + "path": "/logo-hautsdefrance-color.svg", "slug": "hautsdefrance", "name": "Hautsdefrance" }, { "file": "logo-ice-color.svg", - "path": "/type/quais/logo-ice-color.svg", + "path": "/logo-ice-color.svg", "slug": "ice", "name": "ICE" }, { "file": "logo-intercites-color.svg", - "path": "/type/quais/logo-intercites-color.svg", + "path": "/logo-intercites-color.svg", "slug": "intercites", "name": "Intercités" }, { "file": "logo-ler-color.svg", - "path": "/type/quais/logo-ler-color.svg", + "path": "/logo-ler-color.svg", "slug": "ler", "name": "Ler" }, { "file": "logo-lio-color.svg", - "path": "/type/quais/logo-lio-color.svg", + "path": "/logo-lio-color.svg", "slug": "lio", "name": "Lio" }, { "file": "logo-lunea-color.svg", - "path": "/type/quais/logo-lunea-color.svg", + "path": "/logo-lunea-color.svg", "slug": "lunea", "name": "Lunea" }, { "file": "logo-metrolor-color.svg", - "path": "/type/quais/logo-metrolor-color.svg", + "path": "/logo-metrolor-color.svg", "slug": "metrolor", "name": "Metrolor" }, { "file": "logo-mobigo-color.svg", - "path": "/type/quais/logo-mobigo-color.svg", + "path": "/logo-mobigo-color.svg", "slug": "mobigo", "name": "Mobigo" }, { "file": "logo-nomad-color.svg", - "path": "/type/quais/logo-nomad-color.svg", + "path": "/logo-nomad-color.svg", "slug": "nomad", "name": "Nomad" }, { "file": "logo-occitanie-color.svg", - "path": "/type/quais/logo-occitanie-color.svg", + "path": "/logo-occitanie-color.svg", "slug": "occitanie", "name": "Occitanie" }, { "file": "logo-ouigo-color.svg", - "path": "/type/quais/logo-ouigo-color.svg", + "path": "/logo-ouigo-color.svg", "slug": "ouigo", "name": "OUIGO" }, { "file": "logo-ouigo-classique-color.svg", - "path": "/type/quais/logo-ouigo-classique-color.svg", + "path": "/logo-ouigo-classique-color.svg", "slug": "ouigo-classique", "name": "OUIGO Trains Classiques" }, { "file": "logo-remi-color.svg", - "path": "/type/quais/logo-remi-color.svg", + "path": "/logo-remi-color.svg", "slug": "remi", "name": "Remi" }, { "file": "logo-renfeave-color.svg", - "path": "/type/quais/logo-renfeave-color.svg", + "path": "/logo-renfeave-color.svg", "slug": "renfeave", "name": "Renfe AVE" }, { "file": "logo-rer-a-color.svg", - "path": "/type/quais/logo-rer-a-color.svg", + "path": "/logo-rer-a-color.svg", "slug": "rer-a", "name": "RER A" }, { "file": "logo-rer-b-color.svg", - "path": "/type/quais/logo-rer-b-color.svg", + "path": "/logo-rer-b-color.svg", "slug": "rer-b", "name": "RER B" }, { "file": "logo-rer-c-color.svg", - "path": "/type/quais/logo-rer-c-color.svg", + "path": "/logo-rer-c-color.svg", "slug": "rer-c", "name": "RER C" }, { "file": "logo-rer-d-color.svg", - "path": "/type/quais/logo-rer-d-color.svg", + "path": "/logo-rer-d-color.svg", "slug": "rer-d", "name": "RER D" }, { "file": "logo-rer-e-color.svg", - "path": "/type/quais/logo-rer-e-color.svg", + "path": "/logo-rer-e-color.svg", "slug": "rer-e", "name": "RER E" }, { "file": "logo-sbb-color.svg", - "path": "/type/quais/logo-sbb-color.svg", + "path": "/logo-sbb-color.svg", "slug": "sbb", "name": "SBB" }, { "file": "logo-sncb-color.svg", - "path": "/type/quais/logo-sncb-color.svg", + "path": "/logo-sncb-color.svg", "slug": "sncb", "name": "SNCB" }, { "file": "logo-sncf-color.svg", - "path": "/type/quais/logo-sncf-color.svg", + "path": "/logo-sncf-color.svg", "slug": "sncf", "name": "Sncf" }, { "file": "sncf-voyageurs-logo-color.png", - "path": "/type/quais/img/type/sncf-voyageurs-logo-color.png", + "path": "/img/type/sncf-voyageurs-logo-color.png", "slug": "sncf-voyageurs-logo", "name": "SNCF" }, { "file": "logo-sncf1985.svg", - "path": "/type/quais/img/type/logo-sncf1985.svg", + "path": "/img/type/logo-sncf1985.svg", "slug": "sncf1985", "name": "Sncf1985" }, { "file": "logo-sncf1992.svg", - "path": "/type/quais/img/type/logo-sncf1992.svg", + "path": "/img/type/logo-sncf1992.svg", "slug": "sncf1992", "name": "Sncf1992" }, { "file": "logo-t-h.svg", - "path": "/type/quais/img/type/logo-t-h.svg", + "path": "/img/type/logo-t-h.svg", "slug": "t-h", "name": "T H" }, { "file": "logo-t-j.svg", - "path": "/type/quais/img/type/logo-t-j.svg", + "path": "/img/type/logo-t-j.svg", "slug": "t-j", "name": "T J" }, { "file": "logo-t-k.svg", - "path": "/type/quais/img/type/logo-t-k.svg", + "path": "/img/type/logo-t-k.svg", "slug": "t-k", "name": "T K" }, { "file": "logo-t-l.svg", - "path": "/type/quais/img/type/logo-t-l.svg", + "path": "/img/type/logo-t-l.svg", "slug": "t-l", "name": "T L" }, { "file": "logo-t-n.svg", - "path": "/type/quais/img/type/logo-t-n.svg", + "path": "/img/type/logo-t-n.svg", "slug": "t-n", "name": "T N" }, { "file": "logo-t-p.svg", - "path": "/type/quais/img/type/logo-t-p.svg", + "path": "/img/type/logo-t-p.svg", "slug": "t-p", "name": "T P" }, { "file": "logo-t-r.svg", - "path": "/type/quais/img/type/logo-t-r.svg", + "path": "/img/type/logo-t-r.svg", "slug": "t-r", "name": "T R" }, { "file": "logo-t-u.svg", - "path": "/type/quais/img/type/logo-t-u.svg", + "path": "/img/type/logo-t-u.svg", "slug": "t-u", "name": "T U" }, { "file": "logo-teoz.svg", - "path": "/type/quais/img/type/logo-teoz.svg", + "path": "/img/type/logo-teoz.svg", "slug": "teoz", "name": "Teoz" }, { "file": "sncf-voyageurs-logo.png", - "path": "/type/quais/sncf-voyageurs-logo.png", + "path": "/sncf-voyageurs-logo.png", "slug": "ter", "name": "TER" }, { "file": "logo-ter-rhone-alpes.svg", - "path": "/type/quais/logo-ter-rhone-alpes.svg", + "path": "/logo-ter-rhone-alpes.svg", "slug": "ter-rhone-alpes", "name": "TER Rhone Alpes" }, { "file": "logo-teralsace.svg", - "path": "/type/quais/logo-teralsace.svg", + "path": "/logo-teralsace.svg", "slug": "teralsace", "name": "Teralsace" }, { "file": "logo-teraquitaine.svg", - "path": "/type/quais/logo-teraquitaine.svg", + "path": "/logo-teraquitaine.svg", "slug": "teraquitaine", "name": "Teraquitaine" }, { "file": "logo-terbassenormandie.svg", - "path": "/type/quais/logo-terbassenormandie.svg", + "path": "/logo-terbassenormandie.svg", "slug": "terbassenormandie", "name": "Terbassenormandie" }, { "file": "logo-terbourgogne.svg", - "path": "/type/quais/logo-terbourgogne.svg", + "path": "/logo-terbourgogne.svg", "slug": "terbourgogne", "name": "Terbourgogne" }, { "file": "logo-terbretagne.svg", - "path": "/type/quais/logo-terbretagne.svg", + "path": "/logo-terbretagne.svg", "slug": "terbretagne", "name": "Terbretagne" }, { "file": "logo-tercentre.svg", - "path": "/type/quais/logo-tercentre.svg", + "path": "/logo-tercentre.svg", "slug": "tercentre", "name": "Tercentre" }, { "file": "logo-terlanguedocroussillon.svg", - "path": "/type/quais/logo-terlanguedocroussillon.svg", + "path": "/logo-terlanguedocroussillon.svg", "slug": "terlanguedocroussillon", "name": "Terlanguedocroussillon" }, { "file": "logo-termidipyrenees.svg", - "path": "/type/quais/logo-termidipyrenees.svg", + "path": "/logo-termidipyrenees.svg", "slug": "termidipyrenees", "name": "Termidipyrenees" }, { "file": "logo-ternordpasdecalais.svg", - "path": "/type/quais/logo-ternordpasdecalais.svg", + "path": "/logo-ternordpasdecalais.svg", "slug": "ternordpasdecalais", "name": "Ternordpasdecalais" }, { "file": "logo-terpoitoucharentes.svg", - "path": "/type/quais/logo-terpoitoucharentes.svg", + "path": "/logo-terpoitoucharentes.svg", "slug": "terpoitoucharentes", "name": "Terpoitoucharentes" }, { "file": "logo-tgv.svg", - "path": "/type/quais/logo-tgv.svg", + "path": "/logo-tgv.svg", "slug": "tgv", "name": "TGV" }, { "file": "logo-inoui.svg", - "path": "/type/quais/logo-inoui.svg", + "path": "/logo-inoui.svg", "slug": "inoui", "name": "TGV InOui" }, { "file": "logo-lyria.svg", - "path": "/type/quais/logo-lyria.svg", + "path": "/logo-lyria.svg", "slug": "lyria", "name": "TGV Lyria" }, { "file": "logo-thalys.svg", - "path": "/type/quais/logo-thalys.svg", + "path": "/logo-thalys.svg", "slug": "thalys", "name": "Thalys" }, { "file": "logo-thello.svg", - "path": "/type/quais/logo-thello.svg", + "path": "/logo-thello.svg", "slug": "thello", "name": "Thello" }, { "file": "track-car.svg", - "path": "/type/quais/track-car.svg", + "path": "/track-car.svg", "slug": "track-car", "name": "Track Car" }, { "file": "logo-tramtrain.svg", - "path": "/type/quais/logo-tramtrain.svg", + "path": "/logo-tramtrain.svg", "slug": "tramtrain", "name": "Tramtrain" }, { "file": "logo-trenitalia.svg", - "path": "/type/quais/logo-trenitalia.svg", + "path": "/logo-trenitalia.svg", "slug": "trenitalia", "name": "Trenitalia" }, { "file": "logo-zou.svg", - "path": "/type/quais/logo-zou.svg", + "path": "/logo-zou.svg", "slug": "zou", "name": "Zou" } diff --git a/src/components/afficheurs/TgvCompositionQuai.css b/src/components/afficheurs/TgvCompositionQuai.css new file mode 100644 index 0000000..7813562 --- /dev/null +++ b/src/components/afficheurs/TgvCompositionQuai.css @@ -0,0 +1,298 @@ +/* ========================================================= + BANDEAU COMPOSITION TGV (SNCF QUAI - Conforme Image 1 & 2) + ========================================================= */ + +.aff-tgv-comp__container { + position: absolute; + bottom: 0; + left: 0; + width: 1920px; + height: 240px; + background-color: #4a0e4e; /* Couleur Prune / Aubergine SNCF authentique */ + color: #ffffff; + font-family: 'Achemine', 'Avenir', 'Helvetica Neue', Arial, sans-serif; + box-sizing: border-box; + padding: 10px 35px 8px 35px; + display: flex; + flex-direction: column; + justify-content: space-between; + z-index: 25; + overflow: hidden; +} + +/* ========================================================= + TOP ROW : BOX VOIE + RAMES TGV + ========================================================= */ +.aff-tgv-comp__top-row { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: 135px; +} + +/* Box Voie à gauche */ +.aff-tgv-comp__track-box { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 120px; + min-width: 120px; +} + +.aff-tgv-comp__track-pill { + background-color: #ffffff; + color: #4a0e4e; + font-size: 19px; + font-weight: 800; + text-transform: uppercase; + padding: 2px 14px; + border-radius: 12px; + line-height: 1.1; + letter-spacing: 0.5px; +} + +.aff-tgv-comp__track-letter { + color: #ffffff; + font-size: 64px; + font-weight: 800; + line-height: 1; + margin-top: 4px; + margin-bottom: 4px; + text-align: center; +} + +.aff-tgv-comp__track-underline { + width: 65px; + height: 3.5px; + background-color: #ffffff; + border-radius: 2px; +} + +/* Conteneur des rames TGV */ +.aff-tgv-comp__rames-container { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 40px; + flex: 1; +} + +/* Bloc d'une Rame */ +.aff-tgv-comp__rame-block { + display: flex; + flex-direction: column; + align-items: center; +} + +/* Titre Destination au-dessus de la rame (jaune doré) */ +.aff-tgv-comp__dest-title { + color: #f5d130; + font-size: 24px; + font-weight: 700; + line-height: 1.1; + margin-bottom: 6px; + text-align: center; + white-space: nowrap; +} + +/* Ligne des numéros de voitures */ +.aff-tgv-comp__coaches-row { + display: flex; + align-items: center; + width: 100%; + margin-bottom: 4px; +} + +.aff-tgv-comp__coach-num-motrice { + width: 44px; + min-width: 44px; +} + +.aff-tgv-comp__coach-num-box { + flex: 1; + min-width: 44px; + display: flex; + align-items: center; + justify-content: center; + color: #ffffff; + font-size: 19px; + font-weight: 700; + letter-spacing: 0.5px; +} + +/* Graphic de la rame TGV */ +.aff-tgv-comp__train-graphic { + display: flex; + align-items: center; + height: 32px; + width: 100%; +} + +/* Motrice avant */ +.aff-tgv-comp__loco-front { + width: 44px; + height: 30px; + background: linear-gradient(180deg, #813988 0%, #5d1d64 100%); + border-top-left-radius: 16px; + border-bottom-left-radius: 12px; + margin-right: 2px; +} + +/* Corps des voitures */ +.aff-tgv-comp__coaches-body { + display: flex; + height: 30px; + flex: 1; +} + +.aff-tgv-comp__coach-segment { + flex: 1; + min-width: 44px; + height: 30px; + background-color: #b776bd; + border-right: 2px solid #ffffff; + position: relative; +} + +.aff-tgv-comp__coach-segment:first-child { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} + +.aff-tgv-comp__coach-segment:last-child { + border-right: none; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +.aff-tgv-comp__coach-segment--bar { + background-color: #c98cd0; +} + +/* Motrice arrière */ +.aff-tgv-comp__loco-rear { + width: 44px; + height: 30px; + background: linear-gradient(180deg, #813988 0%, #5d1d64 100%); + border-top-right-radius: 16px; + border-bottom-right-radius: 12px; + margin-left: 2px; +} + +/* ========================================================= + LIGNE POINTILLÉE AVEC REPÈRES DE QUAI (Conforme Image 2) + ========================================================= */ +.aff-tgv-comp__reperes-line-wrapper { + position: relative; + width: 100%; + height: 28px; + display: flex; + align-items: center; + margin: 2px 0 4px 0; +} + +.aff-tgv-comp__dotted-line { + position: absolute; + top: 50%; + left: 0; + right: 0; + border-bottom: 2.5px dotted #f5d130; + transform: translateY(-50%); + z-index: 1; +} + +.aff-tgv-comp__reperes-row { + position: relative; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 36px; + width: 100%; + padding-right: 40px; + box-sizing: border-box; + z-index: 2; +} + +.aff-tgv-comp__repere-box { + background-color: #00337f; + border: 1.5px solid #ffffff; + border-radius: 4px; + color: #ffffff; + font-weight: 800; + font-size: 21px; + line-height: 1; + width: 32px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; +} + +/* ========================================================= + BOTTOM ROW : MARQUEE + DIGITAL CLOCK + ========================================================= */ +.aff-tgv-comp__bottom-row { + display: flex; + align-items: center; + justify-content: space-between; + height: 52px; + width: 100%; +} + +.aff-tgv-comp__marquee-wrapper { + flex: 1; + overflow: hidden; + white-space: nowrap; + margin-right: 30px; +} + +.aff-tgv-comp__marquee-text { + display: inline-block; + color: #ffffff; + font-style: italic; + font-weight: 700; + font-size: 29px; + letter-spacing: 1px; + text-transform: uppercase; + animation: affTgvMarquee 25s linear infinite; +} + +@keyframes affTgvMarquee { + 0% { + transform: translateX(100%); + } + 100% { + transform: translateX(-100%); + } +} + +/* Horloge TGV */ +.aff-tgv-comp__clock-wrapper { + display: flex; + align-items: center; +} + +.aff-tgv-comp__clock { + background-color: #00337f; + border: 2px solid #ffffff; + border-radius: 4px; + padding: 3px 14px; + display: flex; + align-items: baseline; + gap: 4px; + font-family: 'Achemine', 'Helvetica Neue', Arial, sans-serif; + font-size: 34px; + font-weight: 700; + line-height: 1; +} + +.aff-tgv-comp__clock-hm { + color: #ffffff; +} + +.aff-tgv-comp__clock-s { + color: #f5a623; + margin-left: 2px; +} diff --git a/src/components/afficheurs/TgvCompositionQuai.js b/src/components/afficheurs/TgvCompositionQuai.js new file mode 100644 index 0000000..08f58c5 --- /dev/null +++ b/src/components/afficheurs/TgvCompositionQuai.js @@ -0,0 +1,158 @@ +import React, { useEffect, useState } from 'react'; +import './TgvCompositionQuai.css'; + +/** + * Icône Bar / Coupe de verre + */ +function BarGlassIcon({ size = 20, color = '#ffffff' }) { + return ( + + + + + ); +} + +/** + * Horloge digitale avec secondes (conformité Image 1 & 2) + */ +function TgvDigitalClock() { + const [now, setNow] = useState(new Date()); + useEffect(() => { + const id = setInterval(() => setNow(new Date()), 1000); + return () => clearInterval(id); + }, []); + const hh = String(now.getHours()).padStart(2, '0'); + const mm = String(now.getMinutes()).padStart(2, '0'); + const ss = String(now.getSeconds()).padStart(2, '0'); + return ( +
+ {hh}:{mm} + {ss} +
+ ); +} + +/** + * Dessin d'une rame TGV de 8 voitures avec motrice avant, arrière et voiture Bar en position 4 + */ +function TgvRameShape({ destination, coaches = ['01', '02', '03', 'bar', '05', '06', '07', '08'] }) { + return ( +
+ {destination && ( +
+ {destination} +
+ )} + + {/* Numéros des voitures */} +
+ {/* Espace motrice avant */} +
+ + {coaches.map((c, idx) => ( +
+ {c === 'bar' ? ( + + ) : ( + {c} + )} +
+ ))} + + {/* Espace motrice arrière */} +
+
+ + {/* Silhouette du train TGV */} +
+ {/* Motrice avant arrondie */} +
+ + {/* Voitures 1 à 8 */} +
+ {coaches.map((c, idx) => ( +
+ ))} +
+ + {/* Motrice arrière arrondie */} +
+
+
+ ); +} + +export default function TgvCompositionQuai({ + platform = '', + destination = '', + destination2 = '', + marqueeMessage = '', + composition = null, + reperes = ['R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] +}) { + const cleanTrack = String(platform || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); + + // Détermination des tranches (Rame 1 et Rame 2) + const isMultiDest = Boolean(destination2 && destination2.trim() !== '' && destination2.toLowerCase() !== destination.toLowerCase()); + + const rame1Coaches = (composition && composition.rame1) || ['01', '02', '03', 'bar', '05', '06', '07', '08']; + const rame2Coaches = (composition && composition.rame2) || ['11', '12', '13', 'bar', '15', '16', '17', '18']; + + const defaultMarquee = marqueeMessage || 'MERCI DE VOTRE COMPRÉHENSION..........................................................................................'; + + return ( +
+ {/* LIGNE DU HAUT : BOX VOIE + VISUALISATION DU TRAIN */} +
+ {/* Indicateur Voie à gauche */} +
+
Voie
+
{cleanTrack ? cleanTrack.toUpperCase() : '-'}
+
+
+ + {/* Visualisation Composition des Rames */} +
+ + +
+
+ + {/* LIGNE POINTILLÉE AVEC REPÈRES DE QUAI (Conforme Image 2) */} +
+
+ {reperes && reperes.length > 0 && ( +
+ {reperes.map((rep, idx) => ( +
+ {rep} +
+ ))} +
+ )} +
+ + {/* LIGNE DU BAS : DÉFILEMENT MESSAGE + HORLOGE */} +
+
+
+ {defaultMarquee} +
+
+
+ +
+
+
+ ); +} diff --git a/src/pages/afficheurs/afl/AFLArrivees.js b/src/pages/afficheurs/afl/AFLArrivees.js index 3765d51..51c3f3b 100644 --- a/src/pages/afficheurs/afl/AFLArrivees.js +++ b/src/pages/afficheurs/afl/AFLArrivees.js @@ -6,7 +6,8 @@ import MarqueeText from '../../../components/MarqueeText'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import { getSourceFromPath } from '../../../services/railwayService'; -import { getSncfTimetable } from '../../../services/sncfApi'; +import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi'; +import { matchStationNameStrict } from '../../../services/gtfsStations'; import AFLSecurityPage from './components/AFLSecurityPage'; import AFLCorrespondancesPage from './components/AFLCorrespondancesPage'; import AFLInformationsPage from './components/AFLInformationsPage'; @@ -219,12 +220,8 @@ export default function AFLArrivees(){ const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`; const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle); if (!gerr && gdata && gdata.length) { - const exactMatch = gdata.find(g => { - const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim(); - const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim(); - return n1 === n2; - }); - const chosen = exactMatch || gdata[0]; + const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay)); + const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null); fetchedType = chosen ? chosen.type : null; fetchedId = chosen ? chosen.id : null; } @@ -662,17 +659,18 @@ export default function AFLArrivees(){ const stopForStation = (stops || []).find(s => { const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null; const nm = s.gare_nom || s.nom || s.name || s.stop_name || ''; - const ns = normalizeName(nm || ''); - const target = normalizeName(targetStationName || ''); - return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target); + return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName)); }) || null; horaire._status = displayStatus; horaire._displayTime = displayTime; - const platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || ''; - horaire._displayPlatform = String(platformVal).trim(); + let platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform || stopForStation.voie)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || horaire.voie || ''; + if (!platformVal) { + platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam); + } + horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); return horaire; }); - }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]); + }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]); // Récupération des données (SNCF GTFS ou Supabase) — avec polling pour synchroniser en continu useEffect(() => { @@ -699,11 +697,13 @@ export default function AFLArrivees(){ } else { const formatted = (sncfData || []).map(t => { const timeStr = t.heure_arrivee || t.heure_depart || t.time || ''; + const arrHM = (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr); + const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); return { ...t, _displayTime: formatTimeNoSeconds(timeStr), - _displayPlatform: t.quai || t.voie || '', - _arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr), + _displayPlatform: plat, + _arrHM: arrHM, _status: t._status || { label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"), detail: '', @@ -784,25 +784,24 @@ export default function AFLArrivees(){ // comme une gare d'arrivée (soit le terminus, soit un arrêt intermédiaire) let displayHoraires = parsedHoraires.slice(); if (gareDisplay) { - const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' ')); displayHoraires = displayHoraires.filter(h => { // Exclure si la gare ciblée est la gare de départ (on veut les arrivées, pas les départs) - const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString()); - if (departName === normalized) return false; + const departName = (h.gare_depart_nom || h.gare_depart || '').toString(); + if (matchStationNameStrict(departName, gareDisplay)) return false; - // Inclure si gare d'arrivée finale correspond - const arriveeName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString()); - if (arriveeName === normalized) return true; + // Inclure si gare d'arrivée finale correspond + const arriveeName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString(); + if (matchStationNameStrict(arriveeName, gareDisplay)) return true; // Chercher dans les arrêts desservis ; ignorer si l'arrêt est l'origine (isFirst ou index==0) const stopsList = getStops(h); if (Array.isArray(stopsList) && stopsList.length) { for (const s of stopsList) { - const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString()); + const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString(); if (!stopName) continue; const isOrigin = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && Number(s.index) === 0)); if (isOrigin) continue; // Si c'est l'origine, ça ne peut pas être une arrivée - if (stopName === normalized) return true; + if (matchStationNameStrict(stopName, gareDisplay)) return true; } } return false; @@ -868,7 +867,7 @@ export default function AFLArrivees(){ const stopsListForH = getStops(h); if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) { const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null; - const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null; + const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null; const stop = byId || byName; if (stop && (stop.heure_arrivee || stop.time || stop.heure)) raw = stop.heure_arrivee || stop.time || stop.heure; } @@ -1300,7 +1299,17 @@ export default function AFLArrivees(){ ).toString().trim(); const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero)); - const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR'); + const isCarTER = ( + op === 'CAR TER' || + op === 'AUTOCAR' || + op.includes('AUTOCAR') || + op.includes('CAR') || + op.includes('BUS') || + rawPlatform === 'AUTOCAR' || + rawPlatform === 'CAR' || + rawPlatform === 'BUS' || + isSub + ); let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure || ''); const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue'; diff --git a/src/pages/afficheurs/afl/AFLArriveesQuais.js b/src/pages/afficheurs/afl/AFLArriveesQuais.js index f54bd70..05c7ffa 100644 --- a/src/pages/afficheurs/afl/AFLArriveesQuais.js +++ b/src/pages/afficheurs/afl/AFLArriveesQuais.js @@ -6,6 +6,7 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import Logo from '../../../components/Logo'; import { getSourceFromPath } from '../../../services/railwayService'; import { getSncfTimetable } from '../../../services/sncfApi'; +import { matchStationNameStrict } from '../../../services/gtfsStations'; import GareVideoprotection from '../../../components/afficheurs/GareVideoprotection'; import AttentifsEnsemble from '../../../components/afficheurs/AttentifsEnsemble'; import AFLInformationsPage from './components/AFLInformationsPage'; @@ -373,22 +374,36 @@ const AFLArriveesQuais = () => { quai: quaiParam }); if (sncfData && sncfData.length > 0) { - const train = sncfData[0]; - const enriched = { - ...train, - _rawTime: train.heure_arrivee || train.heure_depart, - _displayTime: formatTimeNoSeconds(train.heure_arrivee || train.heure_depart), - _status: { - label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"), - detail: '', - delayMinutes: train.retard_minutes || null - }, - _nextStationStatus: { - label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure") - }, - _isToday: true - }; - setNextArrival(enriched); + let matching = sncfData; + if (quaiParam) { + const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim(); + matching = sncfData.filter(t => cleanP(t._displayPlatform || t.quai || t.voie) === cleanP(quaiParam)); + } + const train = matching[0]; + if (train) { + const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || ''; + const enriched = { + ...train, + quai: trainPlatform, + quai_reel: trainPlatform, + voie: trainPlatform, + _displayPlatform: trainPlatform, + _rawTime: train.heure_arrivee || train.heure_depart, + _displayTime: formatTimeNoSeconds(train.heure_arrivee || train.heure_depart), + _status: { + label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"), + detail: '', + delayMinutes: train.retard_minutes || null + }, + _nextStationStatus: { + label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure") + }, + _isToday: true + }; + setNextArrival(enriched); + } else { + setNextArrival(null); + } } else { setNextArrival(null); } @@ -591,25 +606,24 @@ const AFLArriveesQuais = () => { // Filter by station: keep arrivals to this station or stops at this station (excluding origin) let stationFiltered = enriched; if (gareDisplay) { - const normalized = normalizeName(gareDisplay); stationFiltered = enriched.filter(h => { // Exclude if target station is the departure origin - const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString()); - if (departName === normalized) return false; + const departName = (h.gare_depart_nom || h.gare_depart || '').toString(); + if (matchStationNameStrict(departName, gareDisplay)) return false; // Include if target station is the terminus - const arriveeName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString()); - if (arriveeName === normalized) return true; + const arriveeName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString(); + if (matchStationNameStrict(arriveeName, gareDisplay)) return true; // Check intermediate stops const stopsList = getStops(h); if (Array.isArray(stopsList) && stopsList.length) { for (const s of stopsList) { - const stopName = normalizeName((s.gare_nom || s.nom || s.name || '').toString()); + const stopName = (s.gare_nom || s.nom || s.name || '').toString(); if (!stopName) continue; const isOrigin = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && Number(s.index) === 0)); if (isOrigin) continue; - if (stopName === normalized) return true; + if (matchStationNameStrict(stopName, gareDisplay)) return true; } } return false; @@ -676,7 +690,7 @@ const AFLArriveesQuais = () => { if (gareDisplay && Array.isArray(stopsList)) { stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)); } - const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || h.quai_reel || h.voie || h.quai || '').toString().trim(); + const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || h.quai_reel || h.voie || h.quai || '').toString().trim(); if (!quaiParam) return true; const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim(); return cleanP(platform) === cleanP(quaiParam); @@ -862,10 +876,9 @@ const AFLArriveesQuais = () => { let journeyStops = []; if (Array.isArray(stopsList) && stopsList.length > 0) { - const normCurrent = normalizeName(currentStationName); const endIdx = stopsList.findIndex(s => { - const sName = normalizeName((s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString()); - return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName)); + const sName = (s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString(); + return sName && matchStationNameStrict(sName, currentStationName); }); const rawStops = endIdx !== -1 ? stopsList.slice(0, endIdx + 1) : stopsList.slice(); diff --git a/src/pages/afficheurs/afl/AFLDeparts.js b/src/pages/afficheurs/afl/AFLDeparts.js index 7911fe4..883cebe 100644 --- a/src/pages/afficheurs/afl/AFLDeparts.js +++ b/src/pages/afficheurs/afl/AFLDeparts.js @@ -6,7 +6,8 @@ import MarqueeText from '../../../components/MarqueeText'; import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import { getSourceFromPath } from '../../../services/railwayService'; -import { getSncfTimetable } from '../../../services/sncfApi'; +import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi'; +import { matchStationNameStrict } from '../../../services/gtfsStations'; import AFLSecurityPage from './components/AFLSecurityPage'; import AFLCorrespondancesPage from './components/AFLCorrespondancesPage'; import AFLInformationsPage from './components/AFLInformationsPage'; @@ -227,12 +228,8 @@ export default function AFLDeparts(){ const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`; const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle); if (!gerr && gdata && gdata.length) { - const exactMatch = gdata.find(g => { - const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim(); - const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim(); - return n1 === n2; - }); - const chosen = exactMatch || gdata[0]; + const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay)); + const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null); fetchedType = chosen ? chosen.type : null; fetchedId = chosen ? chosen.id : null; } @@ -670,17 +667,18 @@ export default function AFLDeparts(){ const stopForStation = (stops || []).find(s => { const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null; const nm = s.gare_nom || s.nom || s.name || s.stop_name || ''; - const ns = normalizeName(nm || ''); - const target = normalizeName(targetStationName || ''); - return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target); + return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName)); }) || null; horaire._status = displayStatus; horaire._displayTime = displayTime; - const platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || ''; - horaire._displayPlatform = String(platformVal).trim(); + let platformVal = (stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.quai_prevu || stopForStation.platform || stopForStation.voie)) || horaire.quai_reel || horaire.quai || horaire.quai_prevu || horaire.platform || horaire.voie || ''; + if (!platformVal) { + platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam); + } + horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); return horaire; }); - }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]); + }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]); useEffect(() => { const id = setInterval(() => setShowStatus(s => !s), 3000); @@ -710,18 +708,22 @@ export default function AFLDeparts(){ if (sncfErr) { setError(sncfErr); } else { - const formatted = (sncfData || []).map(t => ({ - ...t, - _displayTime: formatTimeNoSeconds(t.heure_depart), - _displayPlatform: t.quai || t.voie || '', - _depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart), - _status: t._status || { - label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"), - detail: '', - delayMinutes: t.retard_minutes || null, - causeDetails: t.cause_retard || null - } - })); + const formatted = (sncfData || []).map(t => { + const depHM = (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart); + const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); + return { + ...t, + _displayTime: formatTimeNoSeconds(t.heure_depart), + _displayPlatform: plat, + _depHM: depHM, + _status: t._status || { + label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"), + detail: '', + delayMinutes: t.retard_minutes || null, + causeDetails: t.cause_retard || null + } + }; + }); setHoraires(formatted); setAllHoraires(formatted); } @@ -795,15 +797,14 @@ export default function AFLDeparts(){ // Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus) let displayHoraires = parsedHoraires.slice(); if (gareDisplay) { - const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' ')); displayHoraires = displayHoraires.filter(h => { // Exclure si la gare ciblée est la gare d'arrivée finale - const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString()); - if (destName === normalized) return false; + const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString(); + if (matchStationNameStrict(destName, gareDisplay)) return false; // Inclure si gare de départ correspond - const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString()); - if (departName === normalized) return true; + const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString(); + if (matchStationNameStrict(departName, gareDisplay)) return true; // Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max) const stopsList = getStops(h); @@ -816,11 +817,11 @@ export default function AFLDeparts(){ } } for (const s of stopsList) { - const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString()); + const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString(); if (!stopName) continue; const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx)); if (isTerminus) continue; - if (stopName === normalized) return true; + if (matchStationNameStrict(stopName, gareDisplay)) return true; } } return false; @@ -893,7 +894,7 @@ export default function AFLDeparts(){ const stopsListForH = getStops(h); if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) { const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null; - const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null; + const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null; const stop = byId || byName; if (stop && (stop.heure_depart || stop.time || stop.heure)) raw = stop.heure_depart || stop.time || stop.heure; } @@ -1325,7 +1326,17 @@ export default function AFLDeparts(){ ).toString().trim(); const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero)); - const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR'); + const isCarTER = ( + op === 'CAR TER' || + op === 'AUTOCAR' || + op.includes('AUTOCAR') || + op.includes('CAR') || + op.includes('BUS') || + rawPlatform === 'AUTOCAR' || + rawPlatform === 'CAR' || + rawPlatform === 'BUS' || + isSub + ); let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || ''); const destination = horaire.gare_arrivee_nom || ''; diff --git a/src/pages/afficheurs/afl/AFLDepartsQuais.js b/src/pages/afficheurs/afl/AFLDepartsQuais.js index 0ceb318..ec17229 100644 --- a/src/pages/afficheurs/afl/AFLDepartsQuais.js +++ b/src/pages/afficheurs/afl/AFLDepartsQuais.js @@ -6,6 +6,7 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import Logo from '../../../components/Logo'; import { getSourceFromPath } from '../../../services/railwayService'; import { getSncfTimetable } from '../../../services/sncfApi'; +import { matchStationNameStrict } from '../../../services/gtfsStations'; import GareVideoprotection from '../../../components/afficheurs/GareVideoprotection'; import AttentifsEnsemble from '../../../components/afficheurs/AttentifsEnsemble'; import DepartEnCoursAFL from '../../../components/afficheurs/DepartEnCoursAFL'; @@ -445,22 +446,36 @@ const AFLDepartsQuais = () => { quai: quaiParam }); if (sncfData && sncfData.length > 0) { - const train = sncfData[0]; - const enriched = { - ...train, - _rawTime: train.heure_depart, - _displayTime: formatTimeNoSeconds(train.heure_depart), - _status: { - label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"), - detail: '', - delayMinutes: train.retard_minutes || null - }, - _nextStationStatus: { - label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure") - }, - _isToday: true - }; - setNextDeparture(enriched); + let matching = sncfData; + if (quaiParam) { + const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim(); + matching = sncfData.filter(t => cleanP(t._displayPlatform || t.quai || t.voie) === cleanP(quaiParam)); + } + const train = matching[0]; + if (train) { + const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || ''; + const enriched = { + ...train, + quai: trainPlatform, + quai_reel: trainPlatform, + voie: trainPlatform, + _displayPlatform: trainPlatform, + _rawTime: train.heure_depart, + _displayTime: formatTimeNoSeconds(train.heure_depart), + _status: { + label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"), + detail: '', + delayMinutes: train.retard_minutes || null + }, + _nextStationStatus: { + label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure") + }, + _isToday: true + }; + setNextDeparture(enriched); + } else { + setNextDeparture(null); + } } else { setNextDeparture(null); } @@ -663,13 +678,12 @@ const AFLDepartsQuais = () => { // Filter by station: keep departures from this station or stops at this station (excluding terminus) let stationFiltered = enriched; if (gareDisplay) { - const normalized = normalizeName(gareDisplay); stationFiltered = enriched.filter(h => { - const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString()); - if (destName === normalized) return false; + const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString(); + if (matchStationNameStrict(destName, gareDisplay)) return false; - const departName = normalizeName((h.gare_depart_nom || h.gare_depart || '').toString()); - if (departName === normalized) return true; + const departName = (h.gare_depart_nom || h.gare_depart || '').toString(); + if (matchStationNameStrict(departName, gareDisplay)) return true; const stopsList = getStops(h); if (Array.isArray(stopsList) && stopsList.length) { @@ -681,11 +695,11 @@ const AFLDepartsQuais = () => { } } for (const s of stopsList) { - const stopName = normalizeName((s.gare_nom || s.nom || s.name || '').toString()); + const stopName = (s.gare_nom || s.nom || s.name || '').toString(); if (!stopName) continue; const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx)); if (isTerminus) continue; - if (stopName === normalized) return true; + if (matchStationNameStrict(stopName, gareDisplay)) return true; } } return false; @@ -751,7 +765,7 @@ const AFLDepartsQuais = () => { if (gareDisplay && Array.isArray(stopsList)) { stopForStation = stopsList.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)); } - const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay)) || h.quai_reel || h.voie || h.quai || '').toString().trim(); + const platform = ((stopForStation && (stopForStation.quai_reel || stopForStation.quai || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || h.quai_reel || h.voie || h.quai || '').toString().trim(); if (!quaiParam) return true; const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim(); return cleanP(platform) === cleanP(quaiParam); @@ -964,10 +978,9 @@ const AFLDepartsQuais = () => { let downstreamStops = []; if (Array.isArray(stopsList) && stopsList.length > 0) { - const normCurrent = normalizeName(currentStationName); const startIdx = stopsList.findIndex(s => { - const sName = normalizeName((s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString()); - return sName && (sName === normCurrent || sName.includes(normCurrent) || normCurrent.includes(sName)); + const sName = (s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '')).toString(); + return sName && matchStationNameStrict(sName, currentStationName); }); const rawDownstream = startIdx !== -1 ? stopsList.slice(startIdx + 1) : stopsList; @@ -976,7 +989,7 @@ const AFLDepartsQuais = () => { rawDownstream.forEach(s => { const name = (s.gare_nom || s.nom || s.name || s.station_name || s.stop_name || (typeof s === 'string' ? s : '')).toString().trim(); const norm = normalizeName(name); - if (name && norm !== normCurrent && !seen.has(norm)) { + if (name && !matchStationNameStrict(name, currentStationName) && !seen.has(norm)) { seen.add(norm); downstreamStops.push(name); } diff --git a/src/pages/afficheurs/classiques/ClassiquesArrivees.css b/src/pages/afficheurs/classiques/ClassiquesArrivees.css index f31b982..7317283 100644 --- a/src/pages/afficheurs/classiques/ClassiquesArrivees.css +++ b/src/pages/afficheurs/classiques/ClassiquesArrivees.css @@ -248,11 +248,25 @@ .aff-classiques-arrivees__status-delayed { color: var(--yellow); - font-size: 30px; - font-weight: 700; - line-height: 1.1; - white-space: nowrap; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; font-family: "Achemine", sans-serif; + line-height: 1.05; +} + +.aff-classiques-arrivees__status-delayed-label { + font-size: 24px; + font-weight: 700; + color: var(--yellow); + text-transform: lowercase; +} + +.aff-classiques-arrivees__status-delayed-time { + font-size: 26px; + font-weight: 700; + color: var(--yellow); } .aff-classiques-arrivees__status-suppressed { diff --git a/src/pages/afficheurs/classiques/ClassiquesArrivees.js b/src/pages/afficheurs/classiques/ClassiquesArrivees.js index 054114d..464e116 100644 --- a/src/pages/afficheurs/classiques/ClassiquesArrivees.js +++ b/src/pages/afficheurs/classiques/ClassiquesArrivees.js @@ -6,9 +6,23 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import { fetchAllRows } from '../../../lib/fetchAllRows'; import { getSourceFromPath } from '../../../services/railwayService'; -import { getSncfTimetable } from '../../../services/sncfApi'; +import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi'; +import { matchStationNameStrict } from '../../../services/gtfsStations'; import { getTrainLogoAndMeta } from '../../../utils/trainLogoHelper'; +// Pictogramme Autocar SNCF / MOBIGO +const AutocarIcon = ({ size = '50px', color = '#ffffff', style = {} }) => ( + + + +); + // Helper utilitaire : format HHhMM sans secondes (ex: 06h44) const formatTimeNoSeconds = (v) => { if (v == null) return ''; @@ -212,12 +226,8 @@ export default function ClassiquesArrivees({ defilement = null } = {}){ const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`; const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle); if (!gerr && gdata && gdata.length) { - const exactMatch = gdata.find(g => { - const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim(); - const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim(); - return n1 === n2; - }); - const chosen = exactMatch || gdata[0]; + const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay)); + const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null); fetchedType = chosen ? chosen.type : null; fetchedId = chosen ? chosen.id : null; } @@ -650,21 +660,20 @@ export default function ClassiquesArrivees({ defilement = null } = {}){ const stopForStation = (stops || []).find(s => { const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null; const nm = s.gare_nom || s.nom || s.name || s.stop_name || ''; - const ns = normalizeName(nm || ''); - const target = normalizeName(targetStationName || ''); - return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target); + return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName)); }) || null; - horaire._status = displayStatus; - horaire._displayTime = displayTime; - // utiliser quai en priorité - horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay)) || horaire.quai || horaire.quai_reel || horaire.platform || ''; + let platformVal = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || horaire.quai || horaire.quai_reel || horaire.platform || horaire.voie || ''; + if (!platformVal) { + platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam); + } + horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); return horaire; }); - }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]); + }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]); useEffect(() => { - // Alternance toutes les 4 secondes (type+numéro ↔ statut) - const id = setInterval(() => setShowStatus(s => !s), 4000); + // Alternance toutes les 2 secondes (type+numéro ↔ statut / suppression) + const id = setInterval(() => setShowStatus(s => !s), 2000); return () => clearInterval(id); }, []); @@ -707,11 +716,13 @@ export default function ClassiquesArrivees({ defilement = null } = {}){ } else { const formatted = (sncfData || []).map(t => { const timeStr = t.heure_arrivee || t.heure_depart || t.time || ''; + const arrHM = (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr); + const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); return { ...t, _displayTime: formatTimeNoSeconds(timeStr), - _displayPlatform: t.quai || t.voie || '', - _arrHM: (typeof t._arrHM === 'number') ? t._arrHM : parseTimeHM(timeStr), + _displayPlatform: plat, + _arrHM: arrHM, _status: t._status || { label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"), detail: '', @@ -720,8 +731,10 @@ export default function ClassiquesArrivees({ defilement = null } = {}){ } }; }); - setHoraires(formatted); - setAllHoraires(formatted); + const maxCount = defilementMode ? 20 : 10; + const limited = (formatted || []).slice(0, maxCount); + setHoraires(limited); + setAllHoraires(limited); } } catch (e) { if (mounted) setError(e.message || String(e)); @@ -791,15 +804,14 @@ export default function ClassiquesArrivees({ defilement = null } = {}){ // Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus) let displayHoraires = parsedHoraires.slice(); if (gareDisplay) { - const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' ')); displayHoraires = displayHoraires.filter(h => { // Exclure si la gare ciblée est la gare de départ - const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString()); - if (departName === normalized) return false; + const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString(); + if (matchStationNameStrict(departName, gareDisplay)) return false; // Inclure si gare d'arrivée correspond - const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString()); - if (destName === normalized) return true; + const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString(); + if (matchStationNameStrict(destName, gareDisplay)) return true; // Chercher dans les arrêts desservis ; ignorer si l'arrêt est le point de départ (isFirst ou index==0) const stopsList = getStops(h); @@ -812,11 +824,11 @@ export default function ClassiquesArrivees({ defilement = null } = {}){ } } for (const s of stopsList) { - const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString()); + const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString(); if (!stopName) continue; const isOrigine = Boolean(s.isFirst || s.is_first || (typeof s.index !== 'undefined' && minIdx != null && Number(s.index) === minIdx)); if (isOrigine) continue; - if (stopName === normalized) return true; + if (matchStationNameStrict(stopName, gareDisplay)) return true; } } return false; @@ -889,7 +901,7 @@ export default function ClassiquesArrivees({ defilement = null } = {}){ const stopsListForH = getStops(h); if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) { const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null; - const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null; + const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null; const stop = byId || byName; if (stop && (stop.heure_arrivee || stop.time || stop.heure)) raw = stop.heure_arrivee || stop.time || stop.heure; } @@ -1165,10 +1177,13 @@ export default function ClassiquesArrivees({ defilement = null } = {}){ // Création des lignes vides pour créer l'espace const emptyRows = Array.from({ length: 10 }).map((_, i) => ({ _isEmpty: true, id: `empty-${i}` })); + const maxRowsCount = defilementMode ? 20 : 10; + const limitedHoraires = (horaires || []).slice(0, maxRowsCount); + // Liste de lignes à rendre (horaires + espace + horaires + espace en mode défilement pour boucle infinie) const rowsToRender = defilementMode - ? [...horaires, ...emptyRows, ...horaires, ...emptyRows] - : horaires; + ? [...limitedHoraires, ...emptyRows, ...limitedHoraires, ...emptyRows] + : limitedHoraires; // Calcul dynamique de l'animation CSS useEffect(() => { @@ -1259,7 +1274,21 @@ export default function ClassiquesArrivees({ defilement = null } = {}){ const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations)); const isSuppressed = status && status.label === 'supprimé'; const isDelayed = status && status.label && String(status.label).startsWith('retardé'); - const showPlatform = !isSuppressed && Boolean(cleanPlatform); + const isCar = ( + (horaire.type_train && (horaire.type_train.toUpperCase().includes('AUTOCAR') || horaire.type_train.toUpperCase().includes('CAR TER') || horaire.type_train.toUpperCase() === 'BUS')) || + (horaire.op && (horaire.op.toUpperCase().includes('AUTOCAR') || horaire.op.toUpperCase().includes('CAR TER') || horaire.op.toUpperCase() === 'BUS')) || + (horaire.type && (horaire.type.toUpperCase().includes('AUTOCAR') || horaire.type.toUpperCase().includes('CAR TER') || horaire.type.toUpperCase() === 'BUS')) || + Boolean(horaire.is_substitution) || + (numTrain && String(numTrain).startsWith('3') && String(numTrain).length >= 5) || + typeLabel === 'AUTOCAR' || + rawPlatform === 'AUTOCAR' || + rawPlatform === 'CAR' || + rawPlatform === 'BUS' || + cleanPlatform === 'AUTOCAR' || + cleanPlatform === 'CAR' || + cleanPlatform === 'BUS' + ); + const showPlatform = !isSuppressed && (isCar || Boolean(cleanPlatform)); const stopsList = isExtended ? getStopsListForArrivees(horaire, gareDisplay || gareParam, normalizeName) : []; @@ -1280,7 +1309,10 @@ export default function ClassiquesArrivees({ defilement = null } = {}){ isSuppressed ? (
supprimé
) : isDelayed ? ( -
retard {status.delayMinutes} min.
+
+
retardé
+
+{status.delayMinutes || horaire.retard_minutes || 0} min.
+
) : (
à l'heure
) @@ -1293,7 +1325,12 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
-
{time}
+
+ {time} +
@@ -1303,7 +1340,11 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
{showPlatform ? (
-
{cleanPlatform}
+ {isCar ? ( + + ) : ( +
{cleanPlatform}
+ )}
) : null}
@@ -1346,7 +1387,10 @@ export default function ClassiquesArrivees({ defilement = null } = {}){ isSuppressed ? (
supprimé
) : isDelayed ? ( -
retard {status.delayMinutes} min.
+
+
retardé
+
+{status.delayMinutes || horaire.retard_minutes || 0} min.
+
) : (
à l'heure
) @@ -1360,7 +1404,12 @@ export default function ClassiquesArrivees({ defilement = null } = {}){ {/* COL 3 : Heure (grande, jaune) */}
-
{time}
+
+ {time} +
{/* COL 4 : Provenance */} @@ -1372,7 +1421,11 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
{showPlatform ? (
-
{cleanPlatform}
+ {isCar ? ( + + ) : ( +
{cleanPlatform}
+ )}
) : null}
diff --git a/src/pages/afficheurs/classiques/ClassiquesDeparts.css b/src/pages/afficheurs/classiques/ClassiquesDeparts.css index e72b6b7..3227e3f 100644 --- a/src/pages/afficheurs/classiques/ClassiquesDeparts.css +++ b/src/pages/afficheurs/classiques/ClassiquesDeparts.css @@ -248,11 +248,25 @@ .aff-classiques-departs__status-delayed { color: var(--yellow); - font-size: 30px; - font-weight: 700; - line-height: 1.1; - white-space: nowrap; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; font-family: "Achemine", sans-serif; + line-height: 1.05; +} + +.aff-classiques-departs__status-delayed-label { + font-size: 24px; + font-weight: 700; + color: var(--yellow); + text-transform: lowercase; +} + +.aff-classiques-departs__status-delayed-time { + font-size: 26px; + font-weight: 700; + color: var(--yellow); } .aff-classiques-departs__status-suppressed { diff --git a/src/pages/afficheurs/classiques/ClassiquesDeparts.js b/src/pages/afficheurs/classiques/ClassiquesDeparts.js index 9101d99..ebf1fd0 100644 --- a/src/pages/afficheurs/classiques/ClassiquesDeparts.js +++ b/src/pages/afficheurs/classiques/ClassiquesDeparts.js @@ -6,9 +6,23 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import { fetchAllRows } from '../../../lib/fetchAllRows'; import { getSourceFromPath } from '../../../services/railwayService'; -import { getSncfTimetable } from '../../../services/sncfApi'; +import { getSncfTimetable, resolveNominalTrack } from '../../../services/sncfApi'; +import { matchStationNameStrict } from '../../../services/gtfsStations'; import { getTrainLogoAndMeta } from '../../../utils/trainLogoHelper'; +// Pictogramme Autocar SNCF / MOBIGO +const AutocarIcon = ({ size = '50px', color = '#ffffff', style = {} }) => ( + + + +); + // Helper utilitaire : format HHhMM sans secondes (ex: 06h44) const formatTimeNoSeconds = (v) => { if (v == null) return ''; @@ -212,12 +226,8 @@ export default function ClassiquesDeparts({ defilement = null } = {}){ const flexNeedle = `%${String(gareDisplay).replace(/[-_+\s]+/g, '%')}%`; const { data: gdata, error: gerr } = await supabase.from('gares').select('id,type,nom').ilike('nom', flexNeedle); if (!gerr && gdata && gdata.length) { - const exactMatch = gdata.find(g => { - const n1 = String(g.nom || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim(); - const n2 = String(gareDisplay || '').normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().replace(/[-_+\s]+/g, ' ').trim(); - return n1 === n2; - }); - const chosen = exactMatch || gdata[0]; + const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay)); + const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null); fetchedType = chosen ? chosen.type : null; fetchedId = chosen ? chosen.id : null; } @@ -650,21 +660,20 @@ export default function ClassiquesDeparts({ defilement = null } = {}){ const stopForStation = (stops || []).find(s => { const sid = s.gare_id || s.gareId || s.id || s.stop_id || s.stopId || null; const nm = s.gare_nom || s.nom || s.name || s.stop_name || ''; - const ns = normalizeName(nm || ''); - const target = normalizeName(targetStationName || ''); - return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && ns === target); + return (targetStationId && String(sid) === String(targetStationId)) || (targetStationName && matchStationNameStrict(nm, targetStationName)); }) || null; - horaire._status = displayStatus; - horaire._displayTime = displayTime; - // utiliser quai en priorité - horaire._displayPlatform = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay)) || horaire.quai || horaire.quai_reel || horaire.platform || ''; + let platformVal = (stopForStation && (stopForStation.quai || stopForStation.quai_reel || stopForStation.platform || stopForStation.quay || stopForStation.voie)) || horaire.quai || horaire.quai_reel || horaire.platform || horaire.voie || ''; + if (!platformVal) { + platformVal = resolveNominalTrack(targetStationName || gareDisplay, horaire.numero_train || horaire.num, targetStationId || gareIdParam); + } + horaire._displayPlatform = String(platformVal || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); return horaire; }); - }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, getStops, normalizeName]); + }, [buildPerturbMap, computeStatusForHoraireAtStop, gareDisplay, gareIdParam, gareParam, getStops, normalizeName]); useEffect(() => { - // Alternance toutes les 4 secondes (type+numéro ↔ statut) - const id = setInterval(() => setShowStatus(s => !s), 4000); + // Alternance toutes les 2 secondes (type+numéro ↔ statut / suppression) + const id = setInterval(() => setShowStatus(s => !s), 2000); return () => clearInterval(id); }, []); @@ -705,20 +714,26 @@ export default function ClassiquesDeparts({ defilement = null } = {}){ if (sncfErr) { setError(sncfErr); } else { - const formatted = (sncfData || []).map(t => ({ - ...t, - _displayTime: formatTimeNoSeconds(t.heure_depart), - _displayPlatform: t.quai || t.voie || '', - _depHM: (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart), - _status: t._status || { - label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"), - detail: '', - delayMinutes: t.retard_minutes || null, - causeDetails: t.cause_retard || null - } - })); - setHoraires(formatted); - setAllHoraires(formatted); + const formatted = (sncfData || []).map(t => { + const depHM = (typeof t._depHM === 'number') ? t._depHM : parseTimeHM(t.heure_depart); + const plat = String(t._displayPlatform || t.quai_reel || t.quai || t.voie || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); + return { + ...t, + _displayTime: formatTimeNoSeconds(t.heure_depart), + _displayPlatform: plat, + _depHM: depHM, + _status: t._status || { + label: t.etat === 'supprime' ? 'supprimé' : (t.retard_minutes > 0 ? `retardé +${t.retard_minutes}min.` : "à l'heure"), + detail: '', + delayMinutes: t.retard_minutes || null, + causeDetails: t.cause_retard || null + } + }; + }); + const maxCount = defilementMode ? 20 : 10; + const limited = (formatted || []).slice(0, maxCount); + setHoraires(limited); + setAllHoraires(limited); } } catch (e) { if (mounted) setError(e.message || String(e)); @@ -788,15 +803,14 @@ export default function ClassiquesDeparts({ defilement = null } = {}){ // Exclure les horaires pour lesquels la gare ciblée est la gare d'arrivée finale (terminus) let displayHoraires = parsedHoraires.slice(); if (gareDisplay) { - const normalized = normalizeName(String(gareDisplay).replace(/-/g, ' ')); displayHoraires = displayHoraires.filter(h => { // Exclure si la gare ciblée est la gare d'arrivée finale - const destName = normalizeName((h.gare_arrivee_nom || h.gare_arrivee || '').toString()); - if (destName === normalized) return false; + const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString(); + if (matchStationNameStrict(destName, gareDisplay)) return false; // Inclure si gare de départ correspond - const departName = normalizeName((h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString()); - if (departName === normalized) return true; + const departName = (h.gare_depart_nom || h.gare_depart_name || h.gare_depart || '').toString(); + if (matchStationNameStrict(departName, gareDisplay)) return true; // Chercher dans les arrêts desservis ; ignorer si l'arrêt est le terminus (isLast ou index==max) const stopsList = getStops(h); @@ -809,11 +823,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){ } } for (const s of stopsList) { - const stopName = normalizeName((s.gare_nom || s.nom || s.name || s.stop_name || '').toString()); + const stopName = (s.gare_nom || s.nom || s.name || s.stop_name || '').toString(); if (!stopName) continue; const isTerminus = Boolean(s.isLast || s.is_last || (typeof s.index !== 'undefined' && maxIdx != null && Number(s.index) === maxIdx)); if (isTerminus) continue; - if (stopName === normalized) return true; + if (matchStationNameStrict(stopName, gareDisplay)) return true; } } return false; @@ -886,7 +900,7 @@ export default function ClassiquesDeparts({ defilement = null } = {}){ const stopsListForH = getStops(h); if ((gareIdParam || gareDisplay) && Array.isArray(stopsListForH)) { const byId = gareIdParam ? stopsListForH.find(s => String(s.gare_id || s.gareId || s.id) === String(gareIdParam)) : null; - const byName = gareDisplay ? stopsListForH.find(s => normalizeName((s.gare_nom || s.nom || s.name || '').toString()) === normalizeName(gareDisplay)) : null; + const byName = gareDisplay ? stopsListForH.find(s => matchStationNameStrict(s.gare_nom || s.nom || s.name || '', gareDisplay)) : null; const stop = byId || byName; if (stop && (stop.heure_depart || stop.time || stop.heure)) raw = stop.heure_depart || stop.time || stop.heure; } @@ -1162,10 +1176,13 @@ export default function ClassiquesDeparts({ defilement = null } = {}){ // Création des lignes vides pour créer l'espace const emptyRows = Array.from({ length: 10 }).map((_, i) => ({ _isEmpty: true, id: `empty-${i}` })); + const maxRowsCount = defilementMode ? 20 : 10; + const limitedHoraires = (horaires || []).slice(0, maxRowsCount); + // Liste de lignes à rendre (horaires + espace + horaires + espace en mode défilement pour boucle infinie) const rowsToRender = defilementMode - ? [...horaires, ...emptyRows, ...horaires, ...emptyRows] - : horaires; + ? [...limitedHoraires, ...emptyRows, ...limitedHoraires, ...emptyRows] + : limitedHoraires; // Calcul dynamique de l'animation CSS useEffect(() => { @@ -1256,7 +1273,21 @@ export default function ClassiquesDeparts({ defilement = null } = {}){ const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations)); const isSuppressed = status && status.label === 'supprimé'; const isDelayed = status && status.label && String(status.label).startsWith('retardé'); - const showPlatform = !isSuppressed && Boolean(cleanPlatform); + const isCar = ( + (horaire.type_train && (horaire.type_train.toUpperCase().includes('AUTOCAR') || horaire.type_train.toUpperCase().includes('CAR TER') || horaire.type_train.toUpperCase() === 'BUS')) || + (horaire.op && (horaire.op.toUpperCase().includes('AUTOCAR') || horaire.op.toUpperCase().includes('CAR TER') || horaire.op.toUpperCase() === 'BUS')) || + (horaire.type && (horaire.type.toUpperCase().includes('AUTOCAR') || horaire.type.toUpperCase().includes('CAR TER') || horaire.type.toUpperCase() === 'BUS')) || + Boolean(horaire.is_substitution) || + (numTrain && String(numTrain).startsWith('3') && String(numTrain).length >= 5) || + typeLabel === 'AUTOCAR' || + rawPlatform === 'AUTOCAR' || + rawPlatform === 'CAR' || + rawPlatform === 'BUS' || + cleanPlatform === 'AUTOCAR' || + cleanPlatform === 'CAR' || + cleanPlatform === 'BUS' + ); + const showPlatform = !isSuppressed && (isCar || Boolean(cleanPlatform)); const stopsList = isExtended ? getStopsListForDeparts(horaire, gareDisplay || gareParam, normalizeName) : []; @@ -1277,7 +1308,10 @@ export default function ClassiquesDeparts({ defilement = null } = {}){ isSuppressed ? (
supprimé
) : isDelayed ? ( -
retard {status.delayMinutes} min.
+
+
retardé
+
+{status.delayMinutes || horaire.retard_minutes || 0} min.
+
) : (
à l'heure
) @@ -1290,7 +1324,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
-
{time}
+
+ {time} +
@@ -1300,7 +1339,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
{showPlatform ? (
-
{cleanPlatform}
+ {isCar ? ( + + ) : ( +
{cleanPlatform}
+ )}
) : null}
@@ -1343,7 +1386,10 @@ export default function ClassiquesDeparts({ defilement = null } = {}){ isSuppressed ? (
supprimé
) : isDelayed ? ( -
retard {status.delayMinutes} min.
+
+
retardé
+
+{status.delayMinutes || horaire.retard_minutes || 0} min.
+
) : (
à l'heure
) @@ -1357,7 +1403,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){ {/* COL 3 : Heure (grande, jaune) */}
-
{time}
+
+ {time} +
{/* COL 4 : Destination */} @@ -1369,7 +1420,11 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
{showPlatform ? (
-
{cleanPlatform}
+ {isCar ? ( + + ) : ( +
{cleanPlatform}
+ )}
) : null}
diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.css b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.css index 5c3baf6..7bde19b 100644 --- a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.css +++ b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.css @@ -37,6 +37,11 @@ body, html { z-index: 2; } +.aff-classique-quai-arr__left--tgv { + bottom: 240px !important; + padding: 32px 48px 20px 48px !important; +} + /* Logo Dynamique (TER Bleu / Opérateur) */ .aff-classique-quai-arr__brand { display: flex; @@ -65,6 +70,7 @@ body, html { .aff-classique-quai-arr__time-row { display: flex; align-items: baseline; + min-height: 100px; margin-bottom: 8px; z-index: 3; } @@ -85,12 +91,18 @@ body, html { margin-left: 20px; } -.aff-classique-quai-arr__time--suppressed, +.aff-classique-quai-arr__status-alt { + font-weight: 700; + font-size: 68px; + line-height: 1; + letter-spacing: -0.5px; + white-space: nowrap; +} + .aff-classique-quai-arr__status--suppressed { color: #d32f2f !important; } -.aff-classique-quai-arr__time--delayed, .aff-classique-quai-arr__status--delayed { color: #f5a623 !important; } @@ -107,6 +119,22 @@ body, html { word-break: break-word; } +.aff-classique-quai-arr__destination--multi { + font-size: 52px !important; + margin-top: 6px !important; + margin-bottom: 4px !important; +} + +/* Services PMR, Vélo, Bar */ +.aff-classique-quai-arr__services { + display: flex; + align-items: center; + gap: 18px; + margin-top: auto; + margin-bottom: 8px; + z-index: 4; +} + /* Info Train (Type + Numéro) */ .aff-classique-quai-arr__train-info { display: flex; @@ -173,6 +201,10 @@ body, html { flex-direction: column; } +.aff-classique-quai-arr__right--tgv { + bottom: 240px !important; +} + .aff-classique-quai-arr__stops-wrapper { position: relative; width: 100%; @@ -258,6 +290,74 @@ body, html { font-size: 44px; } +/* ========================================================= + FOURCHE / BIFURCATION DESSERTE TGV (Conforme Image 2) + ========================================================= */ +.aff-classique-quai-arr__fork-container { + position: relative; + display: flex; + flex-direction: column; + width: 100%; +} + +.aff-classique-quai-arr__fork-split-row { + position: relative; + display: flex; + align-items: flex-start; + min-height: 100px; + padding-left: 65px; + margin-top: 4px; +} + +/* Rail courbe violet (#5c538f) */ +.aff-classique-quai-arr__fork-curve-rail { + position: absolute; + left: 20px; + top: -15px; + width: 125px; + height: 80px; + border-left: 28px solid #5c538f; + border-bottom: 28px solid #5c538f; + border-bottom-left-radius: 35px; + z-index: 1; + pointer-events: none; +} + +/* Branche droite */ +.aff-classique-quai-arr__fork-branch-right { + display: flex; + flex-direction: column; + margin-left: 115px; + margin-top: 10px; + z-index: 3; +} + +/* Libellé "voitures 11 à 18" */ +.aff-classique-quai-arr__fork-coaches-label { + color: #f5a623; + font-size: 22px; + font-weight: 700; + margin-bottom: 4px; + letter-spacing: 0.5px; +} + +/* Boîte terminus branche droite */ +.aff-classique-quai-arr__fork-terminus-box { + background-color: #5c538f; + padding: 8px 30px 8px 20px; + border-radius: 6px; + display: flex; + align-items: center; + gap: 16px; +} + +.aff-classique-quai-arr__fork-terminus-name { + color: #ffffff; + font-size: 44px; + font-weight: 700; + white-space: nowrap; +} + /* ========================================================= FOOTER & HORLOGE (Conforme Image 2) ========================================================= */ diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js index 35ba995..a62c67e 100644 --- a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js +++ b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js @@ -6,10 +6,34 @@ import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime import useAutoAnnonce from '../../../../hooks/useAutoAnnonce'; import { getSourceFromPath } from '../../../../services/railwayService'; import { getSncfTimetable } from '../../../../services/sncfApi'; +import { matchStationNameStrict } from '../../../../services/gtfsStations'; import GareVideoprotection from '../../../../components/afficheurs/GareVideoprotection'; import AttentifsEnsemble from '../../../../components/afficheurs/AttentifsEnsemble'; import ClassiquesInformations from '../ClassiquesInformations'; import ClassiquesArrivees from '../ClassiquesArrivees'; +import TgvCompositionQuai from '../../../../components/afficheurs/TgvCompositionQuai'; + +function TgvServiceIcons() { + return ( +
+ {/* PMR / Fauteuil roulant */} + + + + {/* Vélo */} + + + + + + {/* Bar / Verre */} + + + + +
+ ); +} function DigitalClock() { const [now, setNow] = useState(new Date()); @@ -131,6 +155,7 @@ export default function ClassiquesArriveesQuais() { const [, setHoraireError] = useState(null); const [currentTime, setCurrentTime] = useState(new Date()); const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s) + const [showStatusAlt, setShowStatusAlt] = useState(false); const pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase(); const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true'; @@ -144,6 +169,12 @@ export default function ClassiquesArriveesQuais() { return () => clearInterval(id); }, []); + // Alternance 3 secondes : Heure normale ↔ Statut (pour trains retardés ou supprimés) + useEffect(() => { + const id = setInterval(() => setShowStatusAlt(prev => !prev), 3000); + return () => clearInterval(id); + }, []); + // Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai useEffect(() => { if (selectedHoraire) return; @@ -315,14 +346,29 @@ export default function ClassiquesArriveesQuais() { if (sncfData && sncfData.length > 0) { let matchingTrains = sncfData; if (targetQuaiClean) { - matchingTrains = sncfData.filter(t => cleanPlatform(t.quai || t.voie || t.quai_reel || t._displayPlatform) === targetQuaiClean); + matchingTrains = sncfData.filter(t => cleanPlatform(t._displayPlatform || t.quai || t.voie || t.quai_reel) === targetQuaiClean); } const train = matchingTrains[0]; if (train) { const arrHM = parseTimeHM(train.heure_arrivee || train.time || train.heure); + const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || ''; + const trainNumClean = (train.numero_train || train.num || '').toString(); + + // Recherche d'une deuxième tranche / train couplé au même horaire + const partner = matchingTrains.find(t => { + if (!t || t === train) return false; + const tNum = (t.numero_train || t.num || '').toString(); + const tHM = parseTimeHM(t.heure_arrivee || t.time || t.heure); + return tNum !== trainNumClean && tHM === arrHM; + }); + chosenTrain = { horaire: { ...train, + quai: trainPlatform, + quai_reel: trainPlatform, + voie: trainPlatform, + _displayPlatform: trainPlatform, _status: train._status || { label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"), detail: '', @@ -330,10 +376,11 @@ export default function ClassiquesArriveesQuais() { causeDetails: train.cause_retard || null } }, + partner: partner || null, stop: { gare_nom: train.gare_arrivee_nom || gareDisplay || gareParam, heure_arrivee: train.heure_arrivee || train.time || train.heure, - quai: train.quai || train.voie || quaiParam + quai: trainPlatform }, arrHM: arrHM != null ? arrHM : 0, rawTime: train.heure_arrivee || train.time || train.heure @@ -353,13 +400,14 @@ export default function ClassiquesArriveesQuais() { } else if (gareDisplay) { const { data: gdata } = await supabase.from('gares').select('id,nom').ilike('nom', `%${gareDisplay}%`); if (gdata && gdata.length) { - setResolvedGareId(gdata[0].id); + const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay)); + const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null); + if (chosen) setResolvedGareId(chosen.id); } } const { data: all, error } = await fetchAllRows(supabase.from('horaires').select('*')); if (!error && Array.isArray(all) && all.length > 0) { - const normalizedTarget = gareDisplay ? normalizeName(gareDisplay) : null; const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(now.getDate() + 1); @@ -375,14 +423,14 @@ export default function ClassiquesArriveesQuais() { const stops = getStops(h); let stopMatch = null; - if (normalizedTarget) { - const departNameNorm = normalizeName(h.gare_depart_nom || h.gare_depart || ''); - if (departNameNorm && departNameNorm === normalizedTarget) { + if (gareDisplay) { + const departName = (h.gare_depart_nom || h.gare_depart || '').toString(); + if (matchStationNameStrict(departName, gareDisplay)) { // Target is the departure origin -> not an arrival here continue; } - const arriveeName = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || ''); - if (arriveeName && arriveeName === normalizedTarget) { + const arriveeName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString(); + if (matchStationNameStrict(arriveeName, gareDisplay)) { stopMatch = { heure_arrivee: h.heure_arrivee || h.time || h.heure, gare_nom: h.gare_arrivee_nom || h.gare_arrivee, @@ -391,8 +439,8 @@ export default function ClassiquesArriveesQuais() { } if (!stopMatch && Array.isArray(stops)) { for (const s of stops) { - const sName = normalizeName(typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || '')); - if (sName === normalizedTarget) { + const sName = (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || '')).toString(); + if (matchStationNameStrict(sName, gareDisplay)) { stopMatch = s; break; } @@ -408,7 +456,11 @@ export default function ClassiquesArriveesQuais() { if (!stopMatch) continue; - const platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || ''; + const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_arrivee || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_arrivee || h.time || h.heure; + const arrHM = parseTimeHM(rawTime); + if (arrHM == null) continue; + + let platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || ''; const platClean = cleanPlatform(platform); if (targetQuaiClean) { @@ -417,10 +469,6 @@ export default function ClassiquesArriveesQuais() { } } - const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_arrivee || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_arrivee || h.time || h.heure; - const arrHM = parseTimeHM(rawTime); - if (arrHM == null) continue; - candidates.push({ horaire: h, stop: stopMatch, @@ -448,7 +496,16 @@ export default function ClassiquesArriveesQuais() { : (filteredCandidates.length > 0 ? filteredCandidates.sort((a, b) => a.arrHM - b.arrHM)[0] : null)); if (chosen) { - chosenTrain = chosen; + const partnerCand = filteredCandidates.find(c => { + if (!c || c === chosen) return false; + const cNum = (c.horaire.numero_train || c.horaire.num || '').toString(); + const chosenNum = (chosen.horaire.numero_train || chosen.horaire.num || '').toString(); + return cNum !== chosenNum && c.arrHM === chosen.arrHM; + }); + chosenTrain = { + ...chosen, + partner: partnerCand ? partnerCand.horaire : null + }; } } } catch (err) { @@ -501,8 +558,7 @@ export default function ClassiquesArriveesQuais() { // Filtrer les arrêts jusqu'à la gare courante if (gareDisplay && stopsList.length > 0) { - const normCur = normalizeName(gareDisplay); - const foundIdx = stopsList.findIndex(s => normalizeName(s) === normCur); + const foundIdx = stopsList.findIndex(s => matchStationNameStrict(s, gareDisplay)); if (foundIdx > 0) { stopsList = stopsList.slice(0, foundIdx + 1); } @@ -510,9 +566,8 @@ export default function ClassiquesArriveesQuais() { // Garantir que le terminus est présent en dernier if (destinationName) { - const destNorm = normalizeName(destinationName); - const lastNorm = stopsList.length > 0 ? normalizeName(stopsList[stopsList.length - 1]) : ''; - if (lastNorm !== destNorm) { + const lastStop = stopsList.length > 0 ? stopsList[stopsList.length - 1] : ''; + if (!matchStationNameStrict(lastStop, destinationName)) { stopsList.push(destinationName); } } @@ -545,10 +600,64 @@ export default function ClassiquesArriveesQuais() { } } + const partner = selectedHoraire?.partner; + const destinationName1 = destinationName; + const destinationName2 = partner ? (partner.gare_arrivee_nom || partner.gare_arrivee || partner.destination || partner.dest || '') : (horaire?.second_destination || horaire?.dest2 || horaire?.destination2 || (horaire?.tranches && horaire?.tranches[1]?.destination) || ''); + + const trainType1 = trainType; + const trainType2 = partner ? (partner.type_train || partner.train_type || partner.type || partner.op || 'TGV') : (horaire?.type_train2 || horaire?.train_type2 || (destinationName2 ? trainType : '')); + + const trainNumber1 = trainNumber; + const trainNumber2 = partner ? (partner.numero_train || partner.numero || partner.num || '') : (horaire?.numero_train2 || horaire?.num2 || (horaire?.tranches && horaire?.tranches[1]?.num) || ''); + + const isDifferentDest = Boolean(destinationName1 && destinationName2 && !matchStationNameStrict(destinationName1, destinationName2)); + + // Récupération des arrêts de la deuxième tranche + let stopsList2 = []; + if (partner) { + const rawStops2 = getStops(partner); + if (Array.isArray(rawStops2) && rawStops2.length > 0) { + stopsList2 = rawStops2.map(s => (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''))).filter(Boolean); + } + if (gareDisplay && stopsList2.length > 0) { + const fIdx = stopsList2.findIndex(s => matchStationNameStrict(s, gareDisplay)); + if (fIdx >= 0 && fIdx < stopsList2.length - 1) { + stopsList2 = stopsList2.slice(fIdx + 1); + } + } + if (destinationName2 && !matchStationNameStrict(stopsList2[stopsList2.length - 1] || '', destinationName2)) { + stopsList2.push(destinationName2); + } + } + + // Calcul du tronc commun et des branches de séparation (Fourche Image 2) + let commonStops = []; + let branch1Stops = []; + let branch2Stops = []; + + if (isDifferentDest && stopsList.length > 0 && stopsList2.length > 0) { + let divIdx = 0; + const minLen = Math.min(stopsList.length, stopsList2.length); + while (divIdx < minLen && matchStationNameStrict(stopsList[divIdx], stopsList2[divIdx])) { + commonStops.push(stopsList[divIdx]); + divIdx++; + } + branch1Stops = stopsList.slice(divIdx); + branch2Stops = stopsList2.slice(divIdx); + } + + const isTgv = Boolean( + (trainType1 || '').toUpperCase().includes('TGV') || + (trainType2 || '').toUpperCase().includes('TGV') || + (operator || '').toUpperCase().includes('TGV') || + (brand && (brand.label || '').toUpperCase().includes('TGV')) || + (horaire && ((horaire.type_train || horaire.type || '').toUpperCase().includes('TGV') || (horaire.op || '').toUpperCase().includes('TGV'))) + ); + return (
{/* PANNEAU GAUCHE : BLANC (Conforme Image 2) */} -
+
{/* Logo Couleur Dynamique */}
{brand.label} @@ -559,68 +668,164 @@ export default function ClassiquesArriveesQuais() { {/* Heure et Statut */}
- - {formattedTime || '--:--'} - - - {displayStatusText} - + {(isSuppressed || isDelayed) ? ( + showStatusAlt ? ( + + {displayStatusText} + + ) : ( + + {formattedTime || '--:--'} + + ) + ) : ( + <> + + {formattedTime || '--:--'} + + + {displayStatusText} + + + )}
{/* Destination / Gare d'Arrivée Principale */} -
- {destinationName || 'Aucune arrivée prévue'} +
+ {destinationName1 || 'Aucune arrivée prévue'}
- {/* Type de Train & Numéro */} + {/* Type de Train & Numéro 1 */}
- {trainType && {trainType}} - {trainNumber && {trainNumber}} + {trainType1 && {trainType1}} + {trainNumber1 && {trainNumber1}}
+ {/* Destination 2 si multi-tranche / destinations différentes */} + {isDifferentDest && ( + <> +
+ {destinationName2} +
+
+ {trainType2 && {trainType2}} + {trainNumber2 && {trainNumber2}} +
+ + )} + + {/* Si même destination mais 2 numéros couplés */} + {!isDifferentDest && trainNumber2 && trainNumber2 !== trainNumber1 && ( +
+ {trainType2 && {trainType2}} + {trainNumber2} +
+ )} + + {/* Pictogrammes de services pour TGV (PMR, Vélo, Bar) */} + {isTgv && } + {/* Filigrane Vertical "arrivée" géant (orienté bas vers haut) */}
- {/* PANNEAU DROIT : VERT FORÊT AVEC DESSERTE (Conforme Image 2) */} -
+ {/* PANNEAU DROIT : VERT FORÊT AVEC DESSERTE (Conforme Image 2 & Fourche) */} +
- {stopsList.length > 0 ? ( -
    - {stopsList.map((stopName, idx) => { - const isLast = idx === stopsList.length - 1; - return ( -
  • + {isDifferentDest && commonStops.length > 0 ? ( + /* AFFICHAGE FOURCHE / BIFURCATION */ +
    + {/* Tronc commun */} +
      + {commonStops.map((stopName, idx) => ( +
    • - - {stopName} - + {stopName}
    • - ); - })} -
    - ) : ( -
    - Veuillez consulter les écrans du hall de la gare. + ))} +
+ + {/* Ligne d'embranchement avec courbe */} +
+
+
+
voitures 11 à 18
+
+ + + {branch2Stops[branch2Stops.length - 1] || destinationName2} + +
+
+
+ + {/* Branche principale continuant vers terminus 1 */} + {branch1Stops.length > 0 && ( +
    + {branch1Stops.map((stopName, idx) => { + const isLast = idx === branch1Stops.length - 1; + return ( +
  • + + {stopName} +
  • + ); + })} +
+ )}
+ ) : ( + /* AFFICHAGE STANDARD DESSERTE VERTICALE */ + stopsList.length > 0 ? ( +
    + {stopsList.map((stopName, idx) => { + const isLast = idx === stopsList.length - 1; + return ( +
  • + + + {stopName} + +
  • + ); + })} +
+ ) : ( +
+ Veuillez consulter les écrans du hall de la gare. +
+ ) )}
- {/* BANDEAU BAS : PLEINE LARGEUR & HORLOGE (Conforme Image 2) */} -
-
- {quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai ? `Voie ${cleanPlatform(horaire.quai).toUpperCase()}` : '')} + {/* BANDEAU BAS : TGV COMPOSITION OU BANDEAU STANDARD */} + {isTgv ? ( + + ) : ( +
+
+ {quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai || horaire?.voie || horaire?._displayPlatform ? `Voie ${cleanPlatform(horaire.quai || horaire.voie || horaire._displayPlatform).toUpperCase()}` : '')} +
+
+ +
-
- -
-
+ )}
); } \ No newline at end of file diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css index 95a4a6e..378bda1 100644 --- a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css +++ b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.css @@ -37,6 +37,11 @@ body, html { z-index: 2; } +.aff-classique-quai-dep__left--tgv { + bottom: 240px !important; + padding: 32px 48px 20px 48px !important; +} + /* Logo Dynamique */ .aff-classique-quai-dep__brand { display: flex; @@ -65,6 +70,7 @@ body, html { .aff-classique-quai-dep__time-row { display: flex; align-items: baseline; + min-height: 100px; margin-bottom: 8px; z-index: 3; } @@ -85,12 +91,18 @@ body, html { margin-left: 20px; } -.aff-classique-quai-dep__time--suppressed, +.aff-classique-quai-dep__status-alt { + font-weight: 700; + font-size: 68px; + line-height: 1; + letter-spacing: -0.5px; + white-space: nowrap; +} + .aff-classique-quai-dep__status--suppressed { color: #d32f2f !important; } -.aff-classique-quai-dep__time--delayed, .aff-classique-quai-dep__status--delayed { color: #f5a623 !important; } @@ -107,6 +119,22 @@ body, html { word-break: break-word; } +.aff-classique-quai-dep__destination--multi { + font-size: 52px !important; + margin-top: 6px !important; + margin-bottom: 4px !important; +} + +/* Services PMR, Vélo, Bar */ +.aff-classique-quai-dep__services { + display: flex; + align-items: center; + gap: 18px; + margin-top: auto; + margin-bottom: 8px; + z-index: 4; +} + /* Info Train (Type + Numéro) */ .aff-classique-quai-dep__train-info { display: flex; @@ -173,6 +201,10 @@ body, html { flex-direction: column; } +.aff-classique-quai-dep__right--tgv { + bottom: 240px !important; +} + .aff-classique-quai-dep__stops-wrapper { position: relative; width: 100%; @@ -258,6 +290,74 @@ body, html { font-size: 44px; } +/* ========================================================= + FOURCHE / BIFURCATION DESSERTE TGV (Conforme Image 2) + ========================================================= */ +.aff-classique-quai-dep__fork-container { + position: relative; + display: flex; + flex-direction: column; + width: 100%; +} + +.aff-classique-quai-dep__fork-split-row { + position: relative; + display: flex; + align-items: flex-start; + min-height: 100px; + padding-left: 65px; + margin-top: 4px; +} + +/* Rail courbe violet (#5c538f) */ +.aff-classique-quai-dep__fork-curve-rail { + position: absolute; + left: 20px; + top: -15px; + width: 125px; + height: 80px; + border-left: 28px solid #5c538f; + border-bottom: 28px solid #5c538f; + border-bottom-left-radius: 35px; + z-index: 1; + pointer-events: none; +} + +/* Branche droite */ +.aff-classique-quai-dep__fork-branch-right { + display: flex; + flex-direction: column; + margin-left: 115px; + margin-top: 10px; + z-index: 3; +} + +/* Libellé "voitures 11 à 18" */ +.aff-classique-quai-dep__fork-coaches-label { + color: #f5a623; + font-size: 22px; + font-weight: 700; + margin-bottom: 4px; + letter-spacing: 0.5px; +} + +/* Boîte terminus branche droite (ex: Lille Europe) */ +.aff-classique-quai-dep__fork-terminus-box { + background-color: #5c538f; + padding: 8px 30px 8px 20px; + border-radius: 6px; + display: flex; + align-items: center; + gap: 16px; +} + +.aff-classique-quai-dep__fork-terminus-name { + color: #ffffff; + font-size: 44px; + font-weight: 700; + white-space: nowrap; +} + /* ========================================================= FOOTER & HORLOGE (Conforme Image 1) ========================================================= */ diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js index a844c01..17f1108 100644 --- a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js +++ b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js @@ -6,11 +6,35 @@ import useAnnonceSonoreRealtime from '../../../../hooks/useAnnonceSonoreRealtime import useAutoAnnonce from '../../../../hooks/useAutoAnnonce'; import { getSourceFromPath } from '../../../../services/railwayService'; import { getSncfTimetable } from '../../../../services/sncfApi'; +import { matchStationNameStrict } from '../../../../services/gtfsStations'; import GareVideoprotection from '../../../../components/afficheurs/GareVideoprotection'; import AttentifsEnsemble from '../../../../components/afficheurs/AttentifsEnsemble'; import DepartEnCoursClassique from '../../../../components/afficheurs/DepartEnCoursClassique'; import ClassiquesInformations from '../ClassiquesInformations'; import ClassiquesDeparts from '../ClassiquesDeparts'; +import TgvCompositionQuai from '../../../../components/afficheurs/TgvCompositionQuai'; + +function TgvServiceIcons() { + return ( +
+ {/* PMR / Fauteuil roulant */} + + + + {/* Vélo */} + + + + + + {/* Bar / Verre */} + + + + +
+ ); +} function DigitalClock() { const [now, setNow] = useState(new Date()); @@ -132,6 +156,7 @@ export default function ClassiquesDepartsQuais() { const [, setHoraireError] = useState(null); const [currentTime, setCurrentTime] = useState(new Date()); const [idlePhase, setIdlePhase] = useState(0); // 0 = Attentifs (20s), 1 = Quai (20s), 2 = Information (10s) + const [showStatusAlt, setShowStatusAlt] = useState(false); const pageParam = (searchParams.get('page') || searchParams.get('mode') || '').toLowerCase(); const isVideoprotection = pageParam === 'videoprotection' || searchParams.get('videoprotection') === 'true'; @@ -147,6 +172,12 @@ export default function ClassiquesDepartsQuais() { return () => clearInterval(id); }, []); + // Alternance 3 secondes : Heure normale ↔ Statut (pour trains retardés ou supprimés) + useEffect(() => { + const id = setInterval(() => setShowStatusAlt(prev => !prev), 3000); + return () => clearInterval(id); + }, []); + // Alternance par défaut (20s Attentifs -> 20s Quai -> 10s Information) si aucun train n'est prévu sur le quai useEffect(() => { if (selectedHoraire) return; @@ -317,14 +348,29 @@ export default function ClassiquesDepartsQuais() { if (sncfData && sncfData.length > 0) { let matchingTrains = sncfData; if (targetQuaiClean) { - matchingTrains = sncfData.filter(t => cleanPlatform(t.quai || t.voie || t.quai_reel || t._displayPlatform) === targetQuaiClean); + matchingTrains = sncfData.filter(t => cleanPlatform(t._displayPlatform || t.quai || t.voie || t.quai_reel) === targetQuaiClean); } const train = matchingTrains[0]; if (train) { const depHM = parseTimeHM(train.heure_depart || train.time || train.heure); + const trainPlatform = train._displayPlatform || train.quai || train.voie || quaiParam || ''; + const trainNumClean = (train.numero_train || train.num || '').toString(); + + // Recherche d'une deuxième tranche / train couplé au même horaire + const partner = matchingTrains.find(t => { + if (!t || t === train) return false; + const tNum = (t.numero_train || t.num || '').toString(); + const tHM = parseTimeHM(t.heure_depart || t.time || t.heure); + return tNum !== trainNumClean && tHM === depHM; + }); + chosenTrain = { horaire: { ...train, + quai: trainPlatform, + quai_reel: trainPlatform, + voie: trainPlatform, + _displayPlatform: trainPlatform, _status: train._status || { label: train.etat === 'supprime' ? 'supprimé' : (train.retard_minutes > 0 ? `retardé +${train.retard_minutes}min.` : "à l'heure"), detail: '', @@ -332,10 +378,11 @@ export default function ClassiquesDepartsQuais() { causeDetails: train.cause_retard || null } }, + partner: partner || null, stop: { gare_nom: train.gare_depart_nom || gareDisplay || gareParam, heure_depart: train.heure_depart || train.time || train.heure, - quai: train.quai || train.voie || quaiParam + quai: trainPlatform }, depHM: depHM != null ? depHM : 0, rawTime: train.heure_depart || train.time || train.heure @@ -355,13 +402,14 @@ export default function ClassiquesDepartsQuais() { } else if (gareDisplay) { const { data: gdata } = await supabase.from('gares').select('id,nom').ilike('nom', `%${gareDisplay}%`); if (gdata && gdata.length) { - setResolvedGareId(gdata[0].id); + const exactMatch = gdata.find(g => matchStationNameStrict(g.nom, gareDisplay)); + const chosen = exactMatch || (gdata.length === 1 ? gdata[0] : null); + if (chosen) setResolvedGareId(chosen.id); } } const { data: all, error } = await fetchAllRows(supabase.from('horaires').select('*')); if (!error && Array.isArray(all) && all.length > 0) { - const normalizedTarget = gareDisplay ? normalizeName(gareDisplay) : null; const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(now.getDate() + 1); @@ -377,14 +425,14 @@ export default function ClassiquesDepartsQuais() { const stops = getStops(h); let stopMatch = null; - if (normalizedTarget) { - const destNameNorm = normalizeName(h.gare_arrivee_nom || h.gare_arrivee || ''); - if (destNameNorm && destNameNorm === normalizedTarget) { + if (gareDisplay) { + const destName = (h.gare_arrivee_nom || h.gare_arrivee || '').toString(); + if (matchStationNameStrict(destName, gareDisplay)) { // Destination finale -> terminus ici, pas un départ continue; } - const departName = normalizeName(h.gare_depart_nom || h.gare_depart || ''); - if (departName && departName === normalizedTarget) { + const departName = (h.gare_depart_nom || h.gare_depart || '').toString(); + if (matchStationNameStrict(departName, gareDisplay)) { stopMatch = { heure_depart: h.heure_depart || h.time || h.heure, gare_nom: h.gare_depart_nom || h.gare_depart, @@ -393,8 +441,8 @@ export default function ClassiquesDepartsQuais() { } if (!stopMatch && Array.isArray(stops)) { for (const s of stops) { - const sName = normalizeName(typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || '')); - if (sName === normalizedTarget) { + const sName = (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || '')).toString(); + if (matchStationNameStrict(sName, gareDisplay)) { stopMatch = s; break; } @@ -410,7 +458,11 @@ export default function ClassiquesDepartsQuais() { if (!stopMatch) continue; - const platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || ''; + const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_depart || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_depart || h.time || h.heure; + const depHM = parseTimeHM(rawTime); + if (depHM == null) continue; + + let platform = (stopMatch && (stopMatch.quai_reel || stopMatch.quai || stopMatch.platform || stopMatch.voie)) || h.quai_reel || h.voie || h.platform || h.quai || ''; const platClean = cleanPlatform(platform); if (targetQuaiClean) { @@ -419,10 +471,6 @@ export default function ClassiquesDepartsQuais() { } } - const rawTime = (stopMatch && typeof stopMatch === 'object' && (stopMatch.heure_depart || stopMatch.heure || stopMatch.time || stopMatch.heure_passage)) || h.heure_depart || h.time || h.heure; - const depHM = parseTimeHM(rawTime); - if (depHM == null) continue; - candidates.push({ horaire: h, stop: stopMatch, @@ -450,7 +498,16 @@ export default function ClassiquesDepartsQuais() { : (filteredCandidates.length > 0 ? filteredCandidates.sort((a, b) => a.depHM - b.depHM)[0] : null)); if (chosen) { - chosenTrain = chosen; + const partnerCand = filteredCandidates.find(c => { + if (!c || c === chosen) return false; + const cNum = (c.horaire.numero_train || c.horaire.num || '').toString(); + const chosenNum = (chosen.horaire.numero_train || chosen.horaire.num || '').toString(); + return cNum !== chosenNum && c.depHM === chosen.depHM; + }); + chosenTrain = { + ...chosen, + partner: partnerCand ? partnerCand.horaire : null + }; } } } catch (err) { @@ -503,8 +560,7 @@ export default function ClassiquesDepartsQuais() { // Filtrer les arrêts à partir de la gare courante if (gareDisplay && stopsList.length > 0) { - const normCur = normalizeName(gareDisplay); - const foundIdx = stopsList.findIndex(s => normalizeName(s) === normCur); + const foundIdx = stopsList.findIndex(s => matchStationNameStrict(s, gareDisplay)); if (foundIdx >= 0 && foundIdx < stopsList.length - 1) { stopsList = stopsList.slice(foundIdx + 1); } @@ -512,9 +568,8 @@ export default function ClassiquesDepartsQuais() { // Garantir que le terminus est présent en dernier if (destinationName) { - const destNorm = normalizeName(destinationName); - const lastNorm = stopsList.length > 0 ? normalizeName(stopsList[stopsList.length - 1]) : ''; - if (lastNorm !== destNorm) { + const lastStop = stopsList.length > 0 ? stopsList[stopsList.length - 1] : ''; + if (!matchStationNameStrict(lastStop, destinationName)) { stopsList.push(destinationName); } } @@ -567,10 +622,64 @@ export default function ClassiquesDepartsQuais() { } } + const partner = selectedHoraire?.partner; + const destinationName1 = destinationName; + const destinationName2 = partner ? (partner.gare_arrivee_nom || partner.gare_arrivee || partner.destination || partner.dest || '') : (horaire?.second_destination || horaire?.dest2 || horaire?.destination2 || (horaire?.tranches && horaire?.tranches[1]?.destination) || ''); + + const trainType1 = trainType; + const trainType2 = partner ? (partner.type_train || partner.train_type || partner.type || partner.op || 'TGV') : (horaire?.type_train2 || horaire?.train_type2 || (destinationName2 ? trainType : '')); + + const trainNumber1 = trainNumber; + const trainNumber2 = partner ? (partner.numero_train || partner.numero || partner.num || '') : (horaire?.numero_train2 || horaire?.num2 || (horaire?.tranches && horaire?.tranches[1]?.num) || ''); + + const isDifferentDest = Boolean(destinationName1 && destinationName2 && !matchStationNameStrict(destinationName1, destinationName2)); + + // Récupération des arrêts de la deuxième tranche + let stopsList2 = []; + if (partner) { + const rawStops2 = getStops(partner); + if (Array.isArray(rawStops2) && rawStops2.length > 0) { + stopsList2 = rawStops2.map(s => (typeof s === 'string' ? s : (s.gare_nom || s.nom || s.name || s.stop_name || ''))).filter(Boolean); + } + if (gareDisplay && stopsList2.length > 0) { + const fIdx = stopsList2.findIndex(s => matchStationNameStrict(s, gareDisplay)); + if (fIdx >= 0 && fIdx < stopsList2.length - 1) { + stopsList2 = stopsList2.slice(fIdx + 1); + } + } + if (destinationName2 && !matchStationNameStrict(stopsList2[stopsList2.length - 1] || '', destinationName2)) { + stopsList2.push(destinationName2); + } + } + + // Calcul du tronc commun et des branches de séparation (Fourche Image 2) + let commonStops = []; + let branch1Stops = []; + let branch2Stops = []; + + if (isDifferentDest && stopsList.length > 0 && stopsList2.length > 0) { + let divIdx = 0; + const minLen = Math.min(stopsList.length, stopsList2.length); + while (divIdx < minLen && matchStationNameStrict(stopsList[divIdx], stopsList2[divIdx])) { + commonStops.push(stopsList[divIdx]); + divIdx++; + } + branch1Stops = stopsList.slice(divIdx); + branch2Stops = stopsList2.slice(divIdx); + } + + const isTgv = Boolean( + (trainType1 || '').toUpperCase().includes('TGV') || + (trainType2 || '').toUpperCase().includes('TGV') || + (operator || '').toUpperCase().includes('TGV') || + (brand && (brand.label || '').toUpperCase().includes('TGV')) || + (horaire && ((horaire.type_train || horaire.type || '').toUpperCase().includes('TGV') || (horaire.op || '').toUpperCase().includes('TGV'))) + ); + return (
- {/* PANNEAU GAUCHE : BLANC (Conforme Image 1) */} -
+ {/* PANNEAU GAUCHE : BLANC (Conforme Image 1 & Image 2) */} +
{/* Logo Couleur Dynamique */}
{brand.label} @@ -581,68 +690,164 @@ export default function ClassiquesDepartsQuais() { {/* Heure et Statut */}
- - {formattedTime || '--:--'} - - - {displayStatusText} - + {(isSuppressed || isDelayed) ? ( + showStatusAlt ? ( + + {displayStatusText} + + ) : ( + + {formattedTime || '--:--'} + + ) + ) : ( + <> + + {formattedTime || '--:--'} + + + {displayStatusText} + + + )}
{/* Destination Principale */} -
- {destinationName || 'Aucun départ prévu'} +
+ {destinationName1 || 'Aucun départ prévu'}
- {/* Type de Train & Numéro */} + {/* Type de Train & Numéro 1 */}
- {trainType && {trainType}} - {trainNumber && {trainNumber}} + {trainType1 && {trainType1}} + {trainNumber1 && {trainNumber1}}
+ {/* Destination 2 si multi-tranche / destinations différentes (ex: Nice-Ville ou Bruxelles Midi) */} + {isDifferentDest && ( + <> +
+ {destinationName2} +
+
+ {trainType2 && {trainType2}} + {trainNumber2 && {trainNumber2}} +
+ + )} + + {/* Si même destination mais 2 numéros couplés (ex: 2 TGV départ 16h17 vers Paris) */} + {!isDifferentDest && trainNumber2 && trainNumber2 !== trainNumber1 && ( +
+ {trainType2 && {trainType2}} + {trainNumber2} +
+ )} + + {/* Pictogrammes de services pour TGV (PMR, Vélo, Bar) */} + {isTgv && } + {/* Filigrane Vertical "départ" géant (orienté bas vers haut) */}
- {/* PANNEAU DROIT : BLEU NUIT AVEC DESSERTE (Conforme Image 1) */} -
+ {/* PANNEAU DROIT : BLEU NUIT AVEC DESSERTE (Conforme Image 1 & Fourche Image 2) */} +
- {stopsList.length > 0 ? ( -
    - {stopsList.map((stopName, idx) => { - const isLast = idx === stopsList.length - 1; - return ( -
  • + {isDifferentDest && commonStops.length > 0 ? ( + /* AFFICHAGE FOURCHE / BIFURCATION (Conforme Image 2) */ +
    + {/* Tronc commun */} +
      + {commonStops.map((stopName, idx) => ( +
    • - - {stopName} - + {stopName}
    • - ); - })} -
    - ) : ( -
    - Veuillez consulter les écrans du hall de la gare. + ))} +
+ + {/* Ligne d'embranchement avec courbe */} +
+
+
+
voitures 11 à 18
+
+ + + {branch2Stops[branch2Stops.length - 1] || destinationName2} + +
+
+
+ + {/* Branche principale continuant vers terminus 1 */} + {branch1Stops.length > 0 && ( +
    + {branch1Stops.map((stopName, idx) => { + const isLast = idx === branch1Stops.length - 1; + return ( +
  • + + {stopName} +
  • + ); + })} +
+ )}
+ ) : ( + /* AFFICHAGE STANDARD DESSERTE VERTICALE (Conforme Image 1) */ + stopsList.length > 0 ? ( +
    + {stopsList.map((stopName, idx) => { + const isLast = idx === stopsList.length - 1; + return ( +
  • + + + {stopName} + +
  • + ); + })} +
+ ) : ( +
+ Veuillez consulter les écrans du hall de la gare. +
+ ) )}
- {/* BANDEAU BAS : PLEINE LARGEUR & HORLOGE (Conforme Image 1) */} -
-
- {quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai ? `Voie ${cleanPlatform(horaire.quai).toUpperCase()}` : '')} + {/* BANDEAU BAS : TGV COMPOSITION OU BANDEAU STANDARD */} + {isTgv ? ( + + ) : ( +
+
+ {quaiParam ? `Voie ${cleanPlatform(quaiParam).toUpperCase()}` : (horaire?.quai || horaire?.voie || horaire?._displayPlatform ? `Voie ${cleanPlatform(horaire.quai || horaire.voie || horaire._displayPlatform).toUpperCase()}` : '')} +
+
+ +
-
- -
-
+ )}
); } \ No newline at end of file diff --git a/src/services/gtfsStations.js b/src/services/gtfsStations.js index c2bc916..c46e870 100644 --- a/src/services/gtfsStations.js +++ b/src/services/gtfsStations.js @@ -24569,6 +24569,7 @@ function cleanStationQuery(s) { const STATION_ALIASES = { 'valence tgv': '87763029', 'valence': '87761007', + 'valence ville': '87761007', 'paris nord': '87271007', 'paris gare du nord': '87271007', 'paris est': '87113001', @@ -24581,11 +24582,18 @@ const STATION_ALIASES = { 'austerlitz': '87547000', 'paris bercy': '87686667', 'bercy': '87686667', + 'paris saint lazare': '87384008', + 'saint lazare': '87384008', 'marseille': '87751008', 'marseille saint charles': '87751008', 'marseille st charles': '87751008', + 'marseille blancarde': '87751081', 'lyon part dieu': '87723197', 'lyon perrache': '87722025', + 'lyon jean mace': '87282624', + 'lyon gorge de loup': '87721175', + 'lyon vaise': '87721001', + 'lyon saint paul': '87721159', 'lyon saint exupery': '87762906', 'lyon st exupery': '87762906', 'toulouse': '87611004', @@ -24614,9 +24622,73 @@ const STATION_ALIASES = { 'besancon franche comte tgv': '87300863', 'dijon': '87713040', 'dijon ville': '87713040', + 'dijon centre': '87713040', + 'dijon porte neuve': '87713073', + 'dijon porte-neuve': '87713073', + 'porte neuve': '87713073', + 'cannes': '87757625', + 'cannes la bocca': '87757617', + 'nice': '87756056', + 'nice ville': '87756056', + 'nice riquier': '87756353', + 'nice saint augustin': '87756254', + 'toulon': '87755009', + 'toulon sainte musse': '87742262', 'amiens': '87313874' }; +/** + * Normalise un nom de gare pour comparaison stricte + */ +export function normalizeStation(name) { + if (!name) return ''; + return String(name) + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/^gare\s+d['e]\s+/i, '') + .replace(/[-_/.,()'&]+/g, ' ') + .replace(/\bst\b/g, 'saint') + .replace(/\bste\b/g, 'sainte') + .replace(/\s+/g, ' ') + .trim(); +} + +/** + * Compare strictement deux noms de gares pour éviter la confusion entre + * gares de même ville (ex: "Dijon" vs "Dijon Porte Neuve", "Lyon Part Dieu" vs "Lyon Perrache") + */ +export function matchStationNameStrict(a, b) { + if (!a || !b) return false; + const normA = normalizeStation(a); + const normB = normalizeStation(b); + if (!normA || !normB) return false; + if (normA === normB) return true; + + // Liste des qualificatifs de sous-gares / gares secondaires ou périphériques distinctes + const distinctQualifiers = [ + 'porte neuve', 'part dieu', 'perrache', 'jean mace', 'gorge de loup', 'vaise', + 'saint paul', 'saint augustin', 'riquier', 'la bocca', 'bocca', 'pont du gard', + 'sud de france', 'sainte musse', 'europe', 'flandres', 'chateaucreux', 'bellevue', + 'carnot', 'terrasse', 'estressin', 'roche', 'franche comte tgv', 'montbeliard tgv' + ]; + + // Si l'une contient un qualificatif distinct et pas l'autre -> Ne correspondent PAS + for (const q of distinctQualifiers) { + const aHas = normA.includes(q); + const bHas = normB.includes(q); + if (aHas !== bHas) { + return false; + } + } + + // Si on retire "ville" ou "centre", vérifions si elles sont identiques + const stripGeneric = (s) => s.replace(/\b(ville|centre|gare)\b/g, '').replace(/\s+/g, ' ').trim(); + const sA = stripGeneric(normA); + const sB = stripGeneric(normB); + return sA === sB; +} + export function findGtfsGare(query) { if (!query) return null; const rawQ = String(query).trim(); @@ -24633,7 +24705,11 @@ export function findGtfsGare(query) { if (found) return found; } - // 3. Algorithme de scoring par tokens et similarité + // 3. Recherche par correspondance exacte nette + const exact = GTFS_GARES.find(g => cleanStationQuery(g.nom) === qClean); + if (exact) return exact; + + // 4. Algorithme de scoring par tokens et similarité const qTokens = qClean.split(' ').filter(Boolean); const wantsTgv = qTokens.includes('tgv'); @@ -24649,8 +24725,12 @@ export function findGtfsGare(query) { if (gClean === qClean) { score = 1000; - } else if (gClean.startsWith(qClean)) { - score = 500; + } else if (matchStationNameStrict(g.nom, rawQ)) { + score = 900; + } else if (gClean.startsWith(qClean + ' ')) { + // Sous-gare ayant le préfixe mais plus longue (ex: "dijon porte neuve" quand la requête est "dijon") + // Pénaliser si l'utilisateur a tapé une requête courte exacte + score = 400 - (gTokens.length - qTokens.length) * 50; } else { let allFound = true; let tokenMatches = 0; @@ -24665,15 +24745,15 @@ export function findGtfsGare(query) { } if (allFound) { - score = 200 + tokenMatches * 20 - (gTokens.length - qTokens.length) * 5; + score = 300 + tokenMatches * 20 - (gTokens.length - qTokens.length) * 40; } else if (tokenMatches > 0) { - score = tokenMatches * 15 - (gTokens.length - tokenMatches) * 5; + score = tokenMatches * 15 - (gTokens.length - tokenMatches) * 30; } } if (wantsTgv && hasTgv) score += 150; if (wantsTgv && !hasTgv) score -= 200; - if (!wantsTgv && hasTgv) score -= 50; + if (!wantsTgv && hasTgv) score -= 100; if (!wantsTgv && gClean.includes('ville') && qTokens.length === 1 && gTokens[0] === qTokens[0]) { score += 100; @@ -24707,6 +24787,8 @@ export function searchGtfsGares(query, limit = 20) { if (gClean === qClean) { scored.push({ gare: g, score: 1000 }); + } else if (matchStationNameStrict(g.nom, query)) { + scored.push({ gare: g, score: 950 }); } else if (gClean.startsWith(qClean)) { scored.push({ gare: g, score: 800 }); } else { @@ -24718,9 +24800,9 @@ export function searchGtfsGares(query, limit = 20) { } } if (matchCount === qTokens.length) { - scored.push({ gare: g, score: 500 + matchCount * 10 - gTokens.length }); + scored.push({ gare: g, score: 500 + matchCount * 10 - gTokens.length * 10 }); } else if (matchCount > 0) { - scored.push({ gare: g, score: matchCount * 20 - gTokens.length }); + scored.push({ gare: g, score: matchCount * 20 - gTokens.length * 10 }); } } } diff --git a/src/services/sncfApi.js b/src/services/sncfApi.js index db846a6..e663c6e 100644 --- a/src/services/sncfApi.js +++ b/src/services/sncfApi.js @@ -5,23 +5,81 @@ import { findGtfsGare, searchGtfsGares } from './gtfsStations'; const SNCF_GTFS_RT_TRIP_UPDATES = 'https://proxy.transport.data.gouv.fr/resource/sncf-gtfs-rt-trip-updates'; const SNCF_GTFS_RT_SERVICE_ALERTS = 'https://proxy.transport.data.gouv.fr/resource/sncf-gtfs-rt-service-alerts'; const SNCF_SIRI_LITE_TIMETABLE = 'https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-estimated-timetable'; +const SNCF_SIRI_LITE_SITUATION_EXCHANGE = 'https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-situation-exchange'; +const NAVITIA_BASE_URL = 'https://api.sncf.com/v1'; +const NAVITIA_TOKEN = (typeof process !== 'undefined' && process.env && process.env.REACT_APP_NAVITIA_TOKEN) || 'bf06df97-2ff1-4560-b74c-f127cb70764d'; -// Cache mémoire pour le flux GTFS-RT (TTL = 30s) +// TTL de rafraîchissement des flux temps réel SNCF : 2 minutes (120 secondes) +const REALTIME_CACHE_TTL = 120000; + +// Cache mémoire pour le flux GTFS-RT (TTL = 2 min) let gtfsRtCache = { timestamp: 0, updates: new Map(), alerts: [] }; -// Cache mémoire pour le flux SIRI-Lite (TTL = 30s) +// Cache mémoire pour le flux SIRI-Lite Estimated Timetable (TTL = 2 min) let siriLiteCache = { timestamp: 0, platforms: new Map(), updates: new Map() }; +// Cache mémoire pour le flux SIRI-Lite Situation Exchange (TTL = 2 min) +let situationExchangeCache = { + timestamp: 0, + platforms: new Map(), + updates: new Map(), + alerts: [] +}; + +// Cache mémoire pour l'API OpenData SNCF (TTL = 2 min) +const openDataPlatformsCache = new Map(); + +const MAJOR_HUBS = [ + 'paris', 'lyon', 'dijon', 'marseille', 'lille', 'strasbourg', 'bordeaux', + 'toulouse', 'nantes', 'rennes', 'nice', 'montpellier', 'nimes', 'metz', + 'nancy', 'mulhouse', 'tours', 'le mans', 'rouen', 'clermont ferrand', + 'grenoble', 'chambery', 'avignon', 'valence', 'besancon viotte', + 'saint etienne', 'toulon', 'cannes', 'antibes', 'angers' +]; + +const MAJOR_HUB_UICS = [ + '87713040', '87686006', '87271007', '87391003', '87113001', '87384008', + '87547000', '87686667', '87722025', '87723197', '87751008', '87286005', + '87286013', '87212027', '87581009', '87611004', '87481002', '87471003', + '87756056', '87773002', '87775007', '87192039', '87141002', '87182063', + '87571000', '87444000', '87411017', '87734004', '87747006', '87741009', + '87765008', '87318964', '87763029', '87718007', '87726000', '87755009', + '87757625', '87757674', '87484006' +]; + +export function isMajorHub(stationName, uic = '') { + if (uic && MAJOR_HUB_UICS.includes(String(uic).replace(/\D/g, ''))) return true; + if (!stationName) return false; + const clean = String(stationName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim(); + return MAJOR_HUBS.some(hub => clean === hub || clean.startsWith(hub + ' ') || clean.endsWith(' ' + hub)); +} + +export function isCarNumber(num) { + const d = String(num || '').replace(/\D/g, ''); + return (d.length === 5 && d.startsWith('3')) || (d.length === 6 && d.startsWith('3')); +} + +/** + * Résout la voie pour une circulation : + * - Aucune voie nominale/fictive inventée : seuls les quais temps réel SNCF sont affichés. + * - Les services par autocar affichent 'AUTOCAR' (pictogramme). + */ +export function resolveNominalTrack(stationName, trainNum, uic = '') { + const rawNum = String(trainNum || '').replace(/\D/g, ''); + if (isCarNumber(rawNum)) return 'AUTOCAR'; + return ''; +} + // Helper fetch avec timeout pour ne jamais bloquer l'affichage en cas de réseau lent ou de blocage -async function fetchWithTimeout(url, options = {}, timeoutMs = 7000) { +async function fetchWithTimeout(url, options = {}, timeoutMs = 25000) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeoutMs); try { @@ -40,12 +98,53 @@ function cleanNum(n) { return m ? m[0].replace(/^0+/, '') : String(n).trim(); } +function extractAllTrainNumbers(journeyXml) { + const nums = new Set(); + + // 1. TrainNumberRef + const tnr = journeyXml.match(/([^<]+)<\/TrainNumberRef>/); + if (tnr) { + const c = cleanNum(tnr[1]); + if (c) nums.add(c); + } + + // 2. VehicleJourneyName + const vjn = journeyXml.match(/([^<]+)<\/VehicleJourneyName>/); + if (vjn) { + const c = cleanNum(vjn[1]); + if (c) nums.add(c); + } + + // 3. PublishedLineName if numeric + const pln = journeyXml.match(/([^<]+)<\/PublishedLineName>/); + if (pln) { + const m = pln[1].match(/^\d+$/); + if (m) nums.add(cleanNum(m[0])); + } + + // 4. DatedVehicleJourneyRef / VehicleJourneyRef / FramedVehicleJourneyRef + const refs = journeyXml.match(/<(?:DatedVehicleJourneyRef|VehicleJourneyRef|FramedVehicleJourneyRef)>([\s\S]*?)<\/(?:DatedVehicleJourneyRef|VehicleJourneyRef|FramedVehicleJourneyRef)>/g) || []; + for (const r of refs) { + const vjMatch = r.match(/FR:VehicleJourney::(\d{3,7})/i) || r.match(/VehicleJourney::(\d{3,7})/i) || r.match(/::(\d{3,7})/); + if (vjMatch) { + nums.add(cleanNum(vjMatch[1])); + } + const lrMatch = r.match(/LineRef::(\d{3,7})/i); + if (lrMatch) { + nums.add(cleanNum(lrMatch[1])); + } + } + + return Array.from(nums); +} + /** - * Récupère et décode les flux temps réel SIRI Lite SNCF (Quais réels et horaires estimés) + * Récupère et décode le flux SIRI Lite SNCF officiel en temps réel (circulations dans les 60 prochaines minutes) + * Source : https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-estimated-timetable */ async function fetchSiriLiteData() { const now = Date.now(); - if (now - siriLiteCache.timestamp < 30000 && siriLiteCache.platforms.size > 0) { + if (now - siriLiteCache.timestamp < REALTIME_CACHE_TTL && siriLiteCache.platforms.size > 0) { return siriLiteCache; } @@ -53,79 +152,112 @@ async function fetchSiriLiteData() { const updatesMap = new Map(); try { - const res = await fetchWithTimeout(SNCF_SIRI_LITE_TIMETABLE, {}, 8000); + const res = await fetchWithTimeout(SNCF_SIRI_LITE_TIMETABLE, { + headers: { + 'Accept': 'application/xml, text/xml, */*' + } + }, 25000); + if (res && res.ok) { const xml = await res.text(); const journeys = xml.split(''); for (let i = 1; i < journeys.length; i++) { const j = journeys[i]; - const trainNumMatch = j.match(/([^<]+)<\/TrainNumberRef>/); - const rawTrainNum = trainNumMatch ? trainNumMatch[1].trim() : ''; - const trainNum = cleanNum(rawTrainNum); - const journeyRefMatch = j.match(/([^<]+)<\/DatedVehicleJourneyRef>/) || j.match(/([^<]+)<\/VehicleJourneyRef>/); + // 1. Extraction exhaustive des identifiants et numéros de train + const trainNums = extractAllTrainNumbers(j); + + const journeyRefMatch = j.match(/([^<]+)<\/DatedVehicleJourneyRef>/) || j.match(/([^<]+)<\/VehicleJourneyRef>/) || j.match(/[\s\S]*?([^<]+)<\/DatedVehicleJourneyRef>/); const journeyRef = journeyRefMatch ? journeyRefMatch[1].trim() : ''; - const isCanceled = j.includes('true'); + const isCanceledJourney = j.includes('true') || j.includes('cancelled'); - // Extraire les arrêts et quais (RecordedCall et EstimatedCall) - const callMatches = j.match(/<(RecordedCall|EstimatedCall)>[\s\S]*?<\/\1>/g) || []; + // 2. Extraire tous les arrêts desservis (RecordedCall et EstimatedCall) + const callMatches = j.match(/<(?:RecordedCall|EstimatedCall)>[\s\S]*?<\/(?:RecordedCall|EstimatedCall)>/g) || []; for (const call of callMatches) { - const stopRefMatch = call.match(/[^:]*::(\d+)<\/StopPointRef>/) || call.match(/([^<]+)<\/StopPointRef>/); - const uicMatch = stopRefMatch ? (stopRefMatch[1].match(/\d{7,8}/) || [stopRefMatch[1]]) : null; - const uic = uicMatch ? uicMatch[0] : ''; + // Extraction du code UIC de l'arrêt + const stopRefMatch = call.match(/([^<]+)<\/StopPointRef>/); + let uic = ''; + let uic7 = ''; + if (stopRefMatch) { + const rawRef = stopRefMatch[1]; + const uicDigits = rawRef.match(/\d{7,8}/); + uic = uicDigits ? uicDigits[0] : rawRef.replace(/\D/g, ''); + if (uic.length === 8 && uic.startsWith('87')) { + uic7 = uic.slice(2); + } + } const stopNameMatch = call.match(/([^<]+)<\/StopPointName>/); const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : ''; - const depPlatMatch = call.match(/([^<]+)<\/DeparturePlatformName>/); - const arrPlatMatch = call.match(/([^<]+)<\/ArrivalPlatformName>/); - const platform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : '')).trim(); + // Extraction des quais (Départ, Arrivée, Prévu ou Réel) + const depPlatMatch = call.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName)>([^<]+)<\/(?:DeparturePlatformName|PlannedDeparturePlatformName)>/); + const arrPlatMatch = call.match(/<(?:ArrivalPlatformName|PlannedArrivalPlatformName)>([^<]+)<\/(?:ArrivalPlatformName|PlannedArrivalPlatformName)>/); + const quayMatch = call.match(/([^<]+)<\/QuayRef>/); + let rawPlatform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : (quayMatch ? quayMatch[1] : ''))).trim(); + const platform = rawPlatform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); + + // Extraction des heures et calcul du retard const expDepMatch = call.match(/([^<]+)<\/ExpectedDepartureTime>/); const aimDepMatch = call.match(/([^<]+)<\/AimedDepartureTime>/); + const expArrMatch = call.match(/([^<]+)<\/ExpectedArrivalTime>/); + const aimArrMatch = call.match(/([^<]+)<\/AimedArrivalTime>/); let delayMin = 0; - if (expDepMatch && aimDepMatch) { - const expD = new Date(expDepMatch[1]).getTime(); - const aimD = new Date(aimDepMatch[1]).getTime(); + const expTimeStr = (expDepMatch && expDepMatch[1]) || (expArrMatch && expArrMatch[1]); + const aimTimeStr = (aimDepMatch && aimDepMatch[1]) || (aimArrMatch && aimArrMatch[1]); + + if (expTimeStr && aimTimeStr) { + const expD = new Date(expTimeStr).getTime(); + const aimD = new Date(aimTimeStr).getTime(); if (!isNaN(expD) && !isNaN(aimD)) { delayMin = Math.max(0, Math.round((expD - aimD) / 60000)); } } + const isStopCancelled = isCanceledJourney || call.includes('cancelled') || call.includes('cancelled') || call.includes('true'); + if (platform) { - if (uic) { - if (trainNum) { - platformsMap.set(`${trainNum}_${uic}`, platform); - platformsMap.set(`${uic}_${trainNum}`, platform); + for (const tNum of trainNums) { + if (uic) { + platformsMap.set(`${tNum}_${uic}`, platform); + platformsMap.set(`${uic}_${tNum}`, platform); } - if (rawTrainNum) { - platformsMap.set(`${rawTrainNum}_${uic}`, platform); - platformsMap.set(`${uic}_${rawTrainNum}`, platform); + if (uic7) { + platformsMap.set(`${tNum}_${uic7}`, platform); + platformsMap.set(`${uic7}_${tNum}`, platform); } - if (journeyRef) { + if (stopNameClean) { + platformsMap.set(`${tNum}_${stopNameClean}`, platform); + } + } + if (journeyRef) { + if (uic) { platformsMap.set(`${journeyRef}_${uic}`, platform); platformsMap.set(`${uic}_${journeyRef}`, platform); } - } - if (stopNameClean) { - if (trainNum) platformsMap.set(`${trainNum}_${stopNameClean}`, platform); - if (rawTrainNum) platformsMap.set(`${rawTrainNum}_${stopNameClean}`, platform); - if (journeyRef) platformsMap.set(`${journeyRef}_${stopNameClean}`, platform); + if (stopNameClean) { + platformsMap.set(`${journeyRef}_${stopNameClean}`, platform); + } } } - if (trainNum || rawTrainNum || journeyRef) { - const info = { - platform: platform, - delayMinutes: delayMin, - isCanceled: isCanceled - }; - if (trainNum) updatesMap.set(trainNum, info); - if (rawTrainNum) updatesMap.set(rawTrainNum, info); - if (journeyRef) updatesMap.set(journeyRef, info); + const info = { + platform: platform, + delayMinutes: delayMin, + isCanceled: isStopCancelled + }; + + for (const tNum of trainNums) { + if (uic) updatesMap.set(`${tNum}_${uic}`, info); + updatesMap.set(tNum, info); + } + if (journeyRef) { + if (uic) updatesMap.set(`${journeyRef}_${uic}`, info); + updatesMap.set(journeyRef, info); } } } @@ -137,12 +269,298 @@ async function fetchSiriLiteData() { updates: updatesMap }; } catch (err) { - console.warn('[sncfApi] Notice: SIRI-Lite refresh error/timeout:', err); + console.warn('[sncfApi] Notice: SIRI-Lite Estimated Timetable refresh error/timeout:', err); } return siriLiteCache; } +/** + * Récupère et décode le flux SIRI Lite Situation Exchange SNCF (Circulations à venir, perturbations, changements de voie, suppressions) + * Source : https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-situation-exchange + */ +async function fetchSituationExchangeData() { + const now = Date.now(); + if (now - situationExchangeCache.timestamp < REALTIME_CACHE_TTL && situationExchangeCache.platforms.size > 0) { + return situationExchangeCache; + } + + const platformsMap = new Map(); + const updatesMap = new Map(); + const alertsList = []; + + try { + const res = await fetchWithTimeout(SNCF_SIRI_LITE_SITUATION_EXCHANGE, { + headers: { + 'Accept': 'application/xml, text/xml, */*' + } + }, 12000); + + if (res && res.ok) { + const xml = await res.text(); + const situations = xml.split(''); + + for (let i = 1; i < situations.length; i++) { + const s = situations[i]; + + // 1. Extraction exhaustive du train et de la mission + const trainNums = extractAllTrainNumbers(s); + const trainNumMatch = s.match(/([^<]+)<\/TrainNumberRef>/) || s.match(/([^<]+)<\/VehicleJourneyRef>/) || s.match(/([^<]+)<\/PublishedLineName>/); + const rawTrainNum = trainNumMatch ? trainNumMatch[1].trim() : ''; + const primaryTrainNum = cleanNum(rawTrainNum); + if (primaryTrainNum && !trainNums.includes(primaryTrainNum)) { + trainNums.push(primaryTrainNum); + } + + // 2. Extraction de l'arrêt impacté + const stopRefMatch = s.match(/([^<]+)<\/StopPointRef>/); + let uic = ''; + let uic7 = ''; + if (stopRefMatch) { + const rawRef = stopRefMatch[1]; + const uicDigits = rawRef.match(/\d{7,8}/); + uic = uicDigits ? uicDigits[0] : rawRef.replace(/\D/g, ''); + if (uic.length === 8 && uic.startsWith('87')) { + uic7 = uic.slice(2); + } + } + + const stopNameMatch = s.match(/([^<]+)<\/StopPointName>/); + const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : ''; + + // 3. Extraction des messages en français + const summaryFrMatch = s.match(/]*xml:lang="FR"[^>]*>([^<]+)<\/Summary>/i) || s.match(/]*>([^<]+)<\/Summary>/i); + const summary = summaryFrMatch ? summaryFrMatch[1].trim() : ''; + + const descFrMatch = s.match(/]*xml:lang="FR"[^>]*>([^<]+)<\/Description>/i) || s.match(/]*>([^<]+)<\/Description>/i); + const desc = descFrMatch ? descFrMatch[1].trim() : ''; + + const messageText = desc || summary || ''; + + // 4. Détection de changement de quai / voie + const platTagMatch = s.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName|ArrivalPlatformName|PlannedArrivalPlatformName|QuayRef)>([^<]+)<\//i); + let platform = platTagMatch ? platTagMatch[1].replace(/^(?:voie|quai|track|platform)\s*/i, '').trim() : ''; + + if (!platform && messageText) { + const voieTextMatch = messageText.match(/\b(?:voie|quai)\s+([A-Za-z0-9]+)\b/i); + if (voieTextMatch) { + platform = voieTextMatch[1].trim(); + } + } + + // 5. Détection de suppression + const isCanceled = /supprim|annul|gelöscht|cancelled|interrompu/i.test(messageText) || s.includes('true'); + + if (platform) { + for (const tNum of trainNums) { + if (uic) { + platformsMap.set(`${tNum}_${uic}`, platform); + platformsMap.set(`${uic}_${tNum}`, platform); + } + if (uic7) { + platformsMap.set(`${tNum}_${uic7}`, platform); + platformsMap.set(`${uic7}_${tNum}`, platform); + } + if (stopNameClean) { + platformsMap.set(`${tNum}_${stopNameClean}`, platform); + } + } + } + + const info = { + platform: platform, + delayMinutes: 0, + isCanceled: isCanceled, + cause: summary || desc || '' + }; + + for (const tNum of trainNums) { + if (uic) updatesMap.set(`${tNum}_${uic}`, info); + updatesMap.set(tNum, info); + } + + if (messageText) { + alertsList.push({ + header: summary, + text: desc || summary + }); + } + } + } + + situationExchangeCache = { + timestamp: now, + platforms: platformsMap, + updates: updatesMap, + alerts: alertsList + }; + } catch (err) { + console.warn('[sncfApi] Notice: SIRI-Lite Situation Exchange error/timeout:', err); + } + + return situationExchangeCache; +} + +/** + * Récupère les quais en temps réel depuis l'API OpenData SNCF (Navitia + OpenDataSoft) + */ +async function fetchOpenDataSncfPlatforms(uic, mode = 'departs') { + if (!uic) return new Map(); + const cacheKey = `${uic}_${mode}`; + const now = Date.now(); + if (openDataPlatformsCache.has(cacheKey)) { + const cached = openDataPlatformsCache.get(cacheKey); + if (now - cached.timestamp < 30000) { + return cached.platforms; + } + } + + const platformsMap = new Map(); + + try { + const authHeader = NAVITIA_TOKEN.startsWith('Basic') ? NAVITIA_TOKEN : 'Basic ' + btoa(NAVITIA_TOKEN + ':'); + const uicClean = String(uic).replace(/\D/g, ''); + const isArrival = mode === 'arrivees'; + const navitiaPath = isArrival ? 'arrivals' : 'departures'; + const odsDataset = isArrival ? 'prochaines-arrivees-des-trains-en-gare-temps-reel' : 'prochains-departs-des-trains-en-gare-temps-reel'; + + const navitiaEndpoints = [ + `https://api.sncf.com/v1/coverage/sncf/stop_areas/stop_area:OCE:SA:${uicClean}/${navitiaPath}?count=100`, + `https://api.sncf.com/v1/coverage/sncf/stop_areas/stop_area:SNCF:${uicClean}/${navitiaPath}?count=100`, + `https://api.sncf.com/v1/coverage/sncf/stop_points/stop_point:OCE:SP:${uicClean}/${navitiaPath}?count=100`, + `https://api.navitia.io/v1/coverage/sncf/stop_areas/stop_area:OCE:SA:${uicClean}/${navitiaPath}?count=100`, + `https://api.navitia.io/v1/coverage/sncf/stop_areas/stop_area:${uicClean}/${navitiaPath}?count=100` + ]; + + const [navitiaRes, odsV2Res, odsV1Res] = await Promise.allSettled([ + fetchWithTimeout(navitiaEndpoints[0], { headers: { 'Authorization': authHeader } }, 5000) + .then(async res => { + if (res && res.ok) return res; + return fetchWithTimeout(navitiaEndpoints[1], { headers: { 'Authorization': authHeader } }, 5000); + }) + .then(async res => { + if (res && res.ok) return res; + return fetchWithTimeout(navitiaEndpoints[3], { headers: { 'Authorization': NAVITIA_TOKEN } }, 5000); + }), + fetchWithTimeout(`https://ressources.data.sncf.com/api/explore/v2.1/catalog/datasets/${odsDataset}/records?limit=100&where=search(code_uic%2C%22${uicClean}%22)`, {}, 5000), + fetchWithTimeout(`https://data.sncf.com/api/records/1.0/search/?dataset=${odsDataset}&q=${uicClean}&rows=100`, {}, 5000) + ]); + + // 1. Décodage Navitia (API OpenData SNCF temps réel) + if (navitiaRes.status === 'fulfilled' && navitiaRes.value && navitiaRes.value.ok) { + const data = await navitiaRes.value.json(); + const list = (isArrival ? data.arrivals : data.departures) || []; + for (const item of list) { + const info = item.display_informations || {}; + const trainNum = cleanNum(info.headsign || info.trip_short_name || info.commercial_mode || info.code || ''); + const rawNum = String(info.headsign || info.trip_short_name || info.code || '').replace(/\D/g, ''); + + const stopPoint = item.stop_point || (item.stop_date_time && item.stop_date_time.stop_point) || {}; + let platform = ( + stopPoint.platform_code || + info.platform || + '' + ).toString().trim(); + + if (!platform && stopPoint.label) { + const m = stopPoint.label.match(/(?:voie|quai|track|platform)\s*([a-z0-9]+)/i); + if (m) platform = m[1]; + } + + platform = platform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); + + const rawDt = (item.stop_date_time && (item.stop_date_time.departure_date_time || item.stop_date_time.arrival_date_time || item.stop_date_time.base_departure_date_time || item.stop_date_time.base_arrival_date_time)) || ''; + let timeHM = ''; + if (rawDt.includes('T')) { + const tPart = rawDt.split('T')[1]; + if (tPart && tPart.length >= 4) { + timeHM = `${tPart.slice(0, 2)}:${tPart.slice(2, 4)}`; + } + } + + const destClean = info.direction ? formatStationName(info.direction).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : ''; + + if (platform) { + if (trainNum && uicClean) { + platformsMap.set(`${trainNum}_${uicClean}`, platform); + platformsMap.set(`${uicClean}_${trainNum}`, platform); + } + if (rawNum && uicClean) { + platformsMap.set(`${rawNum}_${uicClean}`, platform); + platformsMap.set(`${uicClean}_${rawNum}`, platform); + } + if (trainNum && destClean) { + platformsMap.set(`${trainNum}_${destClean}`, platform); + } + } + } + } + + // 2. Décodage OpenDataSoft SNCF v2.1 + if (odsV2Res.status === 'fulfilled' && odsV2Res.value && odsV2Res.value.ok) { + const data = await odsV2Res.value.json(); + const records = data.results || data.records || []; + for (const r of records) { + const fields = r.fields || r; + const trainNum = cleanNum(fields.num || fields.numero || fields.train || fields.num_train || ''); + const rawNum = String(fields.num || fields.numero || fields.train || fields.num_train || '').replace(/\D/g, ''); + const platform = String(fields.voie || fields.quai || fields.platform || fields.track || fields.voie_quai || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); + const destClean = fields.destination || fields.orig ? formatStationName(fields.destination || fields.orig).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : ''; + + if (platform) { + if (trainNum && uicClean) { + platformsMap.set(`${trainNum}_${uicClean}`, platform); + platformsMap.set(`${uicClean}_${trainNum}`, platform); + } + if (rawNum && uicClean) { + platformsMap.set(`${rawNum}_${uicClean}`, platform); + platformsMap.set(`${uicClean}_${rawNum}`, platform); + } + if (trainNum && destClean) { + platformsMap.set(`${trainNum}_${destClean}`, platform); + } + } + } + } + + // 3. Décodage OpenDataSoft SNCF v1.0 + if (odsV1Res.status === 'fulfilled' && odsV1Res.value && odsV1Res.value.ok) { + const data = await odsV1Res.value.json(); + const records = data.records || []; + for (const r of records) { + const fields = r.fields || {}; + const trainNum = cleanNum(fields.num || fields.numero || fields.train || fields.num_train || ''); + const rawNum = String(fields.num || fields.numero || fields.train || fields.num_train || '').replace(/\D/g, ''); + const platform = String(fields.voie || fields.quai || fields.platform || fields.track || fields.voie_quai || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); + const destClean = fields.destination || fields.orig ? formatStationName(fields.destination || fields.orig).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : ''; + + if (platform) { + if (trainNum && uicClean) { + platformsMap.set(`${trainNum}_${uicClean}`, platform); + platformsMap.set(`${uicClean}_${trainNum}`, platform); + } + if (rawNum && uicClean) { + platformsMap.set(`${rawNum}_${uicClean}`, platform); + platformsMap.set(`${uicClean}_${rawNum}`, platform); + } + if (trainNum && destClean) { + platformsMap.set(`${trainNum}_${destClean}`, platform); + } + } + } + } + } catch (err) { + console.warn('[sncfApi] Notice: OpenData SNCF platform fetch error:', err); + } + + openDataPlatformsCache.set(cacheKey, { + timestamp: now, + platforms: platformsMap + }); + + return platformsMap; +} + /** * Récupère et décode les mises à jour temps réel GTFS-RT SNCF */ @@ -311,8 +729,8 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { const publicUrl = (typeof process !== 'undefined' && process.env && process.env.PUBLIC_URL) ? process.env.PUBLIC_URL : ''; const stationUrl = `${publicUrl}/data/stations/${uic}.json`; - // Chargement parallèle tolérant aux pannes réseau / CORS - const [jsonRes, rtData, siriData] = await Promise.all([ + // Chargement parallèle tolérant aux pannes réseau / CORS (OpenData SNCF, SIRI-Lite Estimated Timetable, SIRI-Lite Situation Exchange, GTFS-RT) + const [jsonRes, rtData, siriData, sxData, openDataPlatforms] = await Promise.all([ fetch(stationUrl).catch(err => { console.warn(`[sncfApi] Notice: fetch station ${uic} error:`, err); return null; @@ -322,8 +740,16 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { return { updates: new Map(), alerts: [] }; }), fetchSiriLiteData().catch(err => { - console.warn('[sncfApi] Notice: SIRI-Lite error:', err); + console.warn('[sncfApi] Notice: SIRI-Lite Timetable error:', err); return { platforms: new Map(), updates: new Map() }; + }), + fetchSituationExchangeData().catch(err => { + console.warn('[sncfApi] Notice: SIRI-Lite Situation Exchange error:', err); + return { platforms: new Map(), updates: new Map(), alerts: [] }; + }), + fetchOpenDataSncfPlatforms(uic, mode).catch(err => { + console.warn('[sncfApi] Notice: OpenData SNCF platform error:', err); + return new Map(); }) ]); @@ -333,6 +759,8 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { } const data = await jsonRes.json(); + const gareRef = findGtfsGare(uic); + const targetStationNom = (gareRef && gareRef.nom) || (data && data.nom) || ''; const rawList = mode === 'arrivees' ? data.arrivees : data.departs; if (!Array.isArray(rawList) || rawList.length === 0) return null; @@ -362,21 +790,18 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { if (Array.isArray(customDaysRaw)) { customDays = customDaysRaw; } else if (typeof customDaysRaw === 'string') { - try { customDays = JSON.parse(customDaysRaw); } catch (e) {} - } - if (Array.isArray(customDays) && customDays.length > 0) { - const match = customDays.find(item => item.date === targetDateHyphen || item.date === targetYYYYMMDD); - if (match) return Boolean(match.circule); + try { customDays = JSON.parse(customDaysRaw); } catch (_) { customDays = []; } } + if (customDays.includes(targetDateHyphen)) return true; } - // 3. Colonnes circule_lundi, circule_mardi... - const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; - const colName = `circule_${dayNames[targetDay]}`; - if (typeof t[colName] !== 'undefined' && t[colName] !== null) { - const db = t.date_debut ? String(t.date_debut).slice(0, 10) : null; + // 3. Base Supabase : circulation standard par jour de semaine + const daysMap = ['circule_dimanche', 'circule_lundi', 'circule_mardi', 'circule_mercredi', 'circule_jeudi', 'circule_vendredi', 'circule_samedi']; + const colName = daysMap[targetDay]; + if (typeof t[colName] !== 'undefined') { + const dd = t.date_debut ? String(t.date_debut).slice(0, 10) : null; const df = t.date_fin ? String(t.date_fin).slice(0, 10) : null; - if (db && targetDateHyphen < db) return false; + if (dd && targetDateHyphen < dd) return false; if (df && targetDateHyphen > df) return false; return Boolean(t[colName]); } @@ -399,21 +824,37 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { return true; }; - // Helper de résolution de quai - const lookupPlatform = (trainNum, tripId, stationUic, stationName) => { + // Helper de résolution de quai en temps réel (Estimated Timetable + Situation Exchange + OpenData) + const lookupPlatform = (trainNum, tripId, stationUic, stationName, rawTimeStr = '', destName = '') => { const cNum = cleanNum(trainNum); + const rawNum = String(trainNum || '').replace(/\D/g, ''); const cName = stationName ? formatStationName(stationName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : ''; + const cDest = destName ? formatStationName(destName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : ''; + const uicClean = String(stationUic || '').replace(/\D/g, ''); + const uic7 = (uicClean.length === 8 && uicClean.startsWith('87')) ? uicClean.slice(2) : ''; return ( - (cNum && stationUic && siriData.platforms.get(`${cNum}_${stationUic}`)) || - (trainNum && stationUic && siriData.platforms.get(`${trainNum}_${stationUic}`)) || - (stationUic && cNum && siriData.platforms.get(`${stationUic}_${cNum}`)) || - (stationUic && trainNum && siriData.platforms.get(`${stationUic}_${trainNum}`)) || + // 1. SIRI-Lite Estimated Timetable (60 prochaines min - Priorité 1) + (cNum && uicClean && siriData.platforms.get(`${cNum}_${uicClean}`)) || + (rawNum && uicClean && siriData.platforms.get(`${rawNum}_${uicClean}`)) || + (uicClean && cNum && siriData.platforms.get(`${uicClean}_${cNum}`)) || + (cNum && uic7 && siriData.platforms.get(`${cNum}_${uic7}`)) || + (uic7 && cNum && siriData.platforms.get(`${uic7}_${cNum}`)) || (cNum && cName && siriData.platforms.get(`${cNum}_${cName}`)) || - (trainNum && cName && siriData.platforms.get(`${trainNum}_${cName}`)) || - (tripId && stationUic && siriData.platforms.get(`${tripId}_${stationUic}`)) || - (stationUic && tripId && siriData.platforms.get(`${stationUic}_${tripId}`)) || + (rawNum && cName && siriData.platforms.get(`${rawNum}_${cName}`)) || + (tripId && uicClean && siriData.platforms.get(`${tripId}_${uicClean}`)) || (tripId && cName && siriData.platforms.get(`${tripId}_${cName}`)) || + // 2. SIRI-Lite Situation Exchange (Circulations à venir / Changements de quai) + (cNum && uicClean && sxData.platforms.get(`${cNum}_${uicClean}`)) || + (rawNum && uicClean && sxData.platforms.get(`${rawNum}_${uicClean}`)) || + (uicClean && cNum && sxData.platforms.get(`${uicClean}_${cNum}`)) || + (cNum && cName && sxData.platforms.get(`${cNum}_${cName}`)) || + (rawNum && cName && sxData.platforms.get(`${rawNum}_${cName}`)) || + // 3. OpenData SNCF direct + (cNum && uicClean && openDataPlatforms.get(`${cNum}_${uicClean}`)) || + (rawNum && uicClean && openDataPlatforms.get(`${rawNum}_${uicClean}`)) || + (cNum && cName && openDataPlatforms.get(`${cNum}_${cName}`)) || + (cNum && cDest && openDataPlatforms.get(`${cNum}_${cDest}`)) || '' ); }; @@ -423,17 +864,30 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { const rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr); const timeHM = parseTimeHM(rawTime) || 0; - // Recherche de mise à jour GTFS-RT ou SIRI-Lite (valide pour aujourd'hui) + // Recherche de mise à jour GTFS-RT, SIRI-Lite Timetable ou SIRI-Lite Situation Exchange const rt = !isTomorrow ? (rtData.updates.get(t.trip_id) || (t.num ? rtData.updates.get(t.num) : null) || (cleanNum(t.num) ? rtData.updates.get(cleanNum(t.num)) : null)) : null; const siri = !isTomorrow ? (siriData.updates.get(t.num) || (cleanNum(t.num) ? siriData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? siriData.updates.get(t.trip_id) : null)) : null; + const sx = !isTomorrow ? (sxData.updates.get(t.num) || (cleanNum(t.num) ? sxData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? sxData.updates.get(t.trip_id) : null)) : null; - const isCanceled = (rt && rt.isCanceled) || (siri && siri.isCanceled) || false; + const isCanceled = (rt && rt.isCanceled) || (siri && siri.isCanceled) || (sx && sx.isCanceled) || false; const delayMinutes = (rt && rt.delayMinutes > 0) ? rt.delayMinutes : (siri && siri.delayMinutes > 0 ? siri.delayMinutes : 0); - const currentStationName = mode === 'arrivees' ? t.dest : t.orig; - let realVoie = lookupPlatform(t.num, t.trip_id, uic, currentStationName); - if (!realVoie && t.quai && typeof t.quai === 'string') { - realVoie = t.quai.trim(); + const currentStationName = targetStationNom || (data && data.nom) || (mode === 'arrivees' ? t.dest : t.orig); + const destinationStation = mode === 'arrivees' ? t.orig : t.dest; + const isAutocar = ( + (t.type && (t.type.toUpperCase().includes('AUTOCAR') || t.type.toUpperCase().includes('CAR') || t.type.toUpperCase() === 'BUS')) || + (t.type_train && (t.type_train.toUpperCase().includes('AUTOCAR') || t.type_train.toUpperCase().includes('CAR') || t.type_train.toUpperCase() === 'BUS')) || + (t.op && (t.op.toUpperCase().includes('AUTOCAR') || t.op.toUpperCase().includes('CAR') || t.op.toUpperCase() === 'BUS')) || + Boolean(t.is_substitution) || + isCarNumber(t.num || t.numero_train || t.numero) + ); + + let realVoie = lookupPlatform(t.num, t.trip_id, uic, currentStationName, rawTime, destinationStation); + if (!realVoie && (t.quai || t.quai_reel || t.platform || t.voie)) { + realVoie = String(t.quai || t.quai_reel || t.platform || t.voie).replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); + } + if (!realVoie) { + realVoie = isAutocar ? 'AUTOCAR' : resolveNominalTrack(currentStationName, t.num, uic); } let statusLabel = "à l'heure"; @@ -446,14 +900,24 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { etat = 'retarde'; } - let cause = ''; - if (!isTomorrow && delayMinutes > 0 && rtData.alerts.length > 0) { - cause = rtData.alerts[idx % rtData.alerts.length].text; + let cause = (sx && sx.cause) || ''; + if (!isTomorrow && delayMinutes > 0 && !cause) { + if (rtData.alerts.length > 0) { + cause = rtData.alerts[idx % rtData.alerts.length].text; + } else if (sxData.alerts.length > 0) { + cause = sxData.alerts[idx % sxData.alerts.length].text; + } } const formattedStops = (t.stops || []).map((st) => { const stopUic = st.uic || ''; - const stopVoie = lookupPlatform(t.num, t.trip_id, stopUic, st.nom); + let stopVoie = lookupPlatform(t.num, t.trip_id, stopUic, st.nom, st.heure, destinationStation); + if (!stopVoie && (st.quai || st.quai_reel || st.voie || st.platform)) { + stopVoie = String(st.quai || st.quai_reel || st.voie || st.platform).replace(/^(?:voie|quai|track|platform)\s*/i, '').trim(); + } + if (!stopVoie) { + stopVoie = isAutocar ? 'AUTOCAR' : resolveNominalTrack(st.nom, t.num); + } return { uic: stopUic, @@ -580,13 +1044,10 @@ export async function getSncfTimetable({ gareName, mode = 'departs', quai = null let finalTrains = null; if (uic) { finalTrains = await fetchGtfsStationTimetable(uic, mode); - } - - // 2. Si non trouvé ou gare sans fichier, chercher avec le nom - if (!finalTrains || finalTrains.length === 0) { - const rawGare = gare ? gare.nom : gareName; - const match = findGtfsGare(rawGare); - if (match && match.uic && match.uic !== uic) { + } else { + // Si UIC non trouvé directement, chercher par nom + const match = findGtfsGare(gareName); + if (match && match.uic) { finalTrains = await fetchGtfsStationTimetable(match.uic, mode); } }