From 4f3a34b10162da7fe083ef0e1942448be84a1cc0 Mon Sep 17 00:00:00 2001 From: mathis Date: Sun, 30 Aug 2026 21:16:38 +0200 Subject: [PATCH] =?UTF-8?q?RELEASE=20V5.5.0=20:=20Voici=20les=20modificati?= =?UTF-8?q?ons=20apport=C3=A9es=20:?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -> Implémentation des afficheurs "Sécurité"; "Informations" et "Correspondances" pour les afficheurs AFL FONCTIONNALITÉS EN COURS DE DÉVELOPPEMENT : -> REFONTE DU DESIGN DES AFFICHEURS CLASSIQUES --- CHANGELOG.md | 69 ++ nginx.conf | 108 +++ public/vigipirate.png | Bin 0 -> 58372 bytes src/pages/SelectAfficheur.js | 208 ++++- src/pages/afficheurs/afl/AFLArrivees.js | 243 ++++-- src/pages/afficheurs/afl/AFLArriveesQuais.js | 72 +- src/pages/afficheurs/afl/AFLDeparts.js | 243 ++++-- src/pages/afficheurs/afl/AFLDepartsQuais.js | 72 +- .../afl/components/AFLCorrespondancesPage.css | 201 +++++ .../afl/components/AFLCorrespondancesPage.js | 214 +++++ .../afl/components/AFLInformationsPage.css | 242 ++++++ .../afl/components/AFLInformationsPage.js | 185 ++++ .../afl/components/AFLSecurityPage.css | 362 ++++++++ .../afl/components/AFLSecurityPage.js | 262 ++++++ .../afficheurs/afl/components/vigipirate.png | Bin 0 -> 58372 bytes .../classiques/ClassiquesArrivees.js | 66 +- .../classiques/ClassiquesDeparts.js | 66 +- .../quais/ClassiquesArriveesQuais.js | 72 +- .../quais/ClassiquesDepartsQuais.js | 72 +- src/pages/afficheurs/eva/EVAArrivees.js | 98 ++- src/pages/afficheurs/eva/EVADeparts.js | 99 ++- src/services/correspondancesService.js | 811 ++++++++++++++++++ src/services/gtfsStations.js | 4 +- src/services/sncfApi.js | 160 ++-- 24 files changed, 3517 insertions(+), 412 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 nginx.conf create mode 100644 public/vigipirate.png create mode 100644 src/pages/afficheurs/afl/components/AFLCorrespondancesPage.css create mode 100644 src/pages/afficheurs/afl/components/AFLCorrespondancesPage.js create mode 100644 src/pages/afficheurs/afl/components/AFLInformationsPage.css create mode 100644 src/pages/afficheurs/afl/components/AFLInformationsPage.js create mode 100644 src/pages/afficheurs/afl/components/AFLSecurityPage.css create mode 100644 src/pages/afficheurs/afl/components/AFLSecurityPage.js create mode 100644 src/pages/afficheurs/afl/components/vigipirate.png create mode 100644 src/services/correspondancesService.js diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2ca5453 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,69 @@ +# 📋 Changelog / Patchnotes - Afficheurs Ferrovia + +Toutes les modifications notables apportées au projet **Afficheurs Ferrovia** sont consignées dans ce document. + +--- + +## 🚀 [Version 4.0.0] - V4 (2026-08-30) + +### 🌟 Nouveautés majeures +- **Intégration intégrale du GTFS SNCF officiel :** + - Référentiel exhaustif de **3 507 gares et haltes ferroviaires françaises** avec leurs identifiants UIC officiels. + - Traitement de **41 847 circulations ferroviaires** (TGV INOUI, OUIGO, TER, INTERCITÉS, INTERCITÉS DE NUIT, TGV Lyria, ICE, Autocars TER). + - Prise en charge complète des calendriers de circulation réguliers et des dates d'exceptions. +- **Flux Temps Réel SNCF SIRI-Lite & GTFS-RT :** + - Connexion directe aux flux temps réel du portail Open Data Transport (`transport.data.gouv.fr`). + - Récupération en continu des retards estimés, annulations et causes de perturbations. + - Extraction des voies et quais réels de départ et d'arrivée. +- **Résolution et recherche intelligente des gares :** + - Moteur de normalisation tolérant aux tirets, accents, abréviations et majuscules/minuscules (`cleanStationQuery`). + - Système d'alias prioritaires et scoring par tokens pour résoudre instantanément les requêtes comme `Valence-TGV`, `Paris-Montparnasse`, `Lyon-Part-Dieu`, etc. + +### 🛠️ Corrections & Optimisations +- **Isolation stricte des quais par gare :** + - Suppression de la propagation globale du quai sur tout le trajet d'un train (ex: train multi-arrêts). + - La voie affichée correspond strictement à la voie de la gare consultée, et chaque arrêt intermédiaire affiche son propre quai réel. +- **Normalisation des horaires $\ge 24\text{h}$ :** + - Correction des horaires de trains de nuit (ex: `28h15` normalisé en `04h15` / `04:15`). +- **Fiabilité du build :** + - Résolution de 100 % des avertissements ESLint (`react-hooks/exhaustive-deps`, `no-unused-vars`). + - Compilation de production validée avec un statut `Compiled successfully`. + +--- + +## 🚆 [Version 3.0.0] - V3 + +### 🌟 Nouveautés majeures +- **Afficheurs multi-styles :** + - **AFL (Afficheurs Fluides & Lumineux) :** Écrans haute lisibilité modernes bleus et verts pour halls et quais. + - **Classiques :** Écrans d'information voyageurs traditionnels (versions Départs, Arrivées et Quai). + - **EVA :** Écrans vidéo voyageurs enrichis. +- **Module d'annonces sonores :** + - Déclenchement automatique des jingles et annonces vocales lors de modifications d'horaires, retards ou départs imminents. +- **Support des tranches et trains combinés :** + - Affichage élégant des destinations multiples et des séparations de rames en cours de route. + +### 🛠️ Corrections & Optimisations +- Défilement fluide des arrêts intermédiaires sur les écrans de quai. +- Gestion des travaux et des substitutions par autocar. + +--- + +## 🚉 [Version 2.0.0] - V2 + +### 🌟 Nouveautés majeures +- **Écrans dédiés par Quai / Voie :** + - Ajout des vues `ClassiquesDepartsQuais` et `ClassiquesArriveesQuais` avec affichage grand format du prochain départ/arrivée. + - Liste détaillée des arrêts intermédiaires desservis en aval. +- **Synchronisation dynamique :** + - Intégration de la base Supabase avec synchronisation périodique automatique sans rechargement de page. + - Alternance visuelle entre heure prévue, état de retard et informations de perturbation. + +--- + +## 🏁 [Version 1.0.0] - V1 + +### 🌟 Nouveautés initiales +- Création de la plateforme d'affichage ferroviaire inspirée des systèmes PIDS (Passenger Information Display Systems). +- Affichage des départs et arrivées en temps réel avec horloge synchronisée. +- Reproduction fidèle des polices, couleurs et contrastes de la signalétique ferroviaire. diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..778e622 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,108 @@ +# ============================================================================== +# Configuration Nginx pour Afficheurs Ferroviaires (React PWA) +# ============================================================================== + +# ------------------------------------------------------------------------------ +# OPTION 1 : Reverse Proxy vers PM2 / Node.js (port 4010) +# ------------------------------------------------------------------------------ +server { + listen 80; + listen [::]:80; + server_name your-domain.com; # Remplacez par votre nom de domaine ou IP + + # Redirection HTTP vers HTTPS (à décommenter en production avec SSL) + # return 301 https://$host$request_uri; + + # Compression Gzip + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/x-javascript + application/xml + application/xml+rss + image/svg+xml; + + location / { + proxy_pass http://127.0.0.1:4010; + proxy_http_version 1.1; + + # En-têtes pour WebSockets et Realtime (Supabase / SSE) + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + + # En-têtes standards + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + } +} + +# ------------------------------------------------------------------------------ +# OPTION 2 : Service direct des fichiers statiques (React Build SPA) +# ------------------------------------------------------------------------------ +# server { +# listen 80; +# listen [::]:80; +# server_name your-domain.com; +# +# # Chemin absolu vers le dossier build de l'application +# root /var/www/afficheurs-ferrovia/build; +# index index.html; +# +# # Compression Gzip +# gzip on; +# gzip_vary on; +# gzip_min_length 1024; +# gzip_proxied any; +# gzip_comp_level 6; +# gzip_types +# text/plain +# text/css +# text/xml +# text/javascript +# application/json +# application/javascript +# application/x-javascript +# application/xml +# application/xml+rss +# image/svg+xml; +# +# # Routage SPA React (indispensable pour /afficheurs/*, /departs, /arrivees, etc.) +# location / { +# try_files $uri $uri/ /index.html; +# } +# +# # Cache pour les assets statiques avec hash (JS/CSS) +# location ~* \.(?:css|js)$ { +# expires 1y; +# access_log off; +# add_header Cache-Control "public, max-age=31536000, immutable"; +# } +# +# # Cache pour les médias, polices et images +# location ~* \.(?:ico|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|mp3|wav)$ { +# expires 30d; +# access_log off; +# add_header Cache-Control "public, max-age=2592000"; +# } +# +# # Pas de cache pour index.html, manifest.json et service-worker (mise à jour PWA instantanée) +# location ~* (?:index\.html|manifest\.json|service-worker\.js)$ { +# expires -1; +# add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; +# } +# } diff --git a/public/vigipirate.png b/public/vigipirate.png new file mode 100644 index 0000000000000000000000000000000000000000..f3320ece82a88acd4bd924008725f5490546dc47 GIT binary patch literal 58372 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

h!ZlgPy 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%0D1`Uv4X4(Bb%*pe+S}a!drF&b3T;y%%jZgA{OM$ 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 { + setAflPages(prev => ({ ...prev, [key]: !prev[key] })); + }; + + const selectAllAflPages = (all) => { + setAflPages({ + horaires: true, + infos: all, + securite1: all, + securite2: all, + corresp: all + }); + }; + // Decode and format station name for display const decodedGare = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') @@ -49,7 +72,19 @@ export default function SelectAfficheur({ onBack, navigate }) { const handleGeneral = () => { if (typeof navigate === 'function') { const kebab = gareParam ? String(gareParam).trim() : ''; - navigate(`${prefix}/afficheurs/${modeParam}/${typeParam}?gare=${encodeURIComponent(kebab)}`); + let url = `${prefix}/afficheurs/${modeParam}/${typeParam}?gare=${encodeURIComponent(kebab)}`; + + if (typeParam === 'afl') { + const activeKeys = Object.entries(aflPages) + .filter(([_, active]) => active) + .map(([key]) => key); + + if (activeKeys.length > 0) { + url += `&pages=${activeKeys.join(',')}`; + } + } + + navigate(url); } }; @@ -65,9 +100,16 @@ export default function SelectAfficheur({ onBack, navigate }) { } }; + const handleSinglePage = (pageKey) => { + if (typeof navigate === 'function') { + const kebab = gareParam ? String(gareParam).trim() : ''; + navigate(`${prefix}/afficheurs/${modeParam}/${typeParam}?gare=${encodeURIComponent(kebab)}&page=${pageKey}`); + } + }; + return (

- Pour la gare de {decodedGare} en mode {modeParam === 'arrivees' ? 'Arrivées' : 'Départs'}. + Pour la gare de {decodedGare} en mode {modeParam === 'arrivees' ? 'Arrivées' : 'Départs'} (Style : {typeParam.toUpperCase()}).

@@ -117,11 +159,112 @@ export default function SelectAfficheur({ onBack, navigate }) { alignItems: 'flex-start', background: '#fafbfc' }}> -
+

Afficheur Général

-

- Affiche la liste complète de tous les trains programmés pour cette gare. +

+ Affiche la liste des trains programmés avec possibilité d'inclure les pages d'informations et services.

+ + {typeParam === 'afl' && ( +
+
+ + Pages à inclure dans la rotation : + +
+ + | + +
+
+ +
+ + + + + + + + + +
+
+ )}
+ {typeParam === 'afl' && ( +
+

+ Accès direct à une page AFL spécifique (fixe) : +

+
+ + + + + + +
+
+ )} +
); } + diff --git a/src/pages/afficheurs/afl/AFLArrivees.js b/src/pages/afficheurs/afl/AFLArrivees.js index 4a117c7..3765d51 100644 --- a/src/pages/afficheurs/afl/AFLArrivees.js +++ b/src/pages/afficheurs/afl/AFLArrivees.js @@ -7,6 +7,9 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import { getSourceFromPath } from '../../../services/railwayService'; import { getSncfTimetable } from '../../../services/sncfApi'; +import AFLSecurityPage from './components/AFLSecurityPage'; +import AFLCorrespondancesPage from './components/AFLCorrespondancesPage'; +import AFLInformationsPage from './components/AFLInformationsPage'; // Helper utilitaire : format HHhMM sans secondes (ex: 06h44) const formatTimeNoSeconds = (v) => { @@ -132,11 +135,35 @@ export default function AFLArrivees(){ // gérer les +, décoder et remplacer les tirets par des espaces const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null; - // Support pour ?page=1, ?page=2 pour verrouiller l'affichage sur une page spécifique - const pageParam = params.get('page'); - const lockedPageIndex = pageParam !== null && !isNaN(parseInt(pageParam, 10)) - ? Math.max(0, parseInt(pageParam, 10) - 1) - : null; + // Support pour verrouiller l'affichage sur une page spécifique via l'URL (?page=...) + // ?page=1 -> Horaires Page 1 + // ?page=2 -> Horaires Page 2 + // ?page=3 | ?page=info | ?page=informations -> Informations + // ?page=4 | ?page=securite | ?page=securite1 -> Sécurité (Image 2) + // ?page=securite2 -> Sécurité (Image 3) + // ?page=5 | ?page=correspondances -> Correspondances + const rawPageParam = params.get('page') ? String(params.get('page')).toLowerCase().trim() : null; + + const parseLockedStep = (param) => { + if (!param) return null; + if (param === '1' || param === 'p1' || param === 'horaires1' || param === 'horaires_1') return { type: 'HORAIRES_1' }; + if (param === '2' || param === 'p2' || param === 'horaires2' || param === 'horaires_2') return { type: 'HORAIRES_2' }; + if (param === '3' || param === 'info' || param === 'information' || param === 'informations' || param === 'infos') return { type: 'INFORMATIONS' }; + if (param === '4' || param === 'securite' || param === 'security' || param === 'safety' || param === 'securite1' || param === 'securite_1') return { type: 'SECURITE', variant: 1 }; + if (param === 'securite2' || param === 'securite_2' || param === 'vigipirate' || param === '3117') return { type: 'SECURITE', variant: 2 }; + if (param === '5' || param === 'correspondance' || param === 'correspondances' || param === 'corresp') return { type: 'CORRESPONDANCES' }; + const n = parseInt(param, 10); + if (!isNaN(n)) { + if (n === 1) return { type: 'HORAIRES_1' }; + if (n === 2) return { type: 'HORAIRES_2' }; + if (n === 3) return { type: 'INFORMATIONS' }; + if (n === 4) return { type: 'SECURITE', variant: 1 }; + if (n >= 5) return { type: 'CORRESPONDANCES' }; + } + return null; + }; + + const lockedStep = parseLockedStep(rawPageParam); // debug flag activable via ?debug=1 or ?debug=true const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true'); @@ -158,7 +185,7 @@ export default function AFLArrivees(){ const [perturbations, setPerturbations] = useState([]); // eslint-disable-next-line no-unused-vars const [stationType, setStationType] = useState(null); - const [, setTravaux] = useState([]); + const [travaux, setTravaux] = useState([]); const [travauxSubstitutions, setTravauxSubstitutions] = useState([]); const [, setLoading] = useState(false); const [, setError] = useState(null); @@ -685,7 +712,7 @@ export default function AFLArrivees(){ } }; }); - setHoraires(formatted.slice(0, 10)); + setHoraires(formatted); setAllHoraires(formatted); } } catch (e) { @@ -861,30 +888,54 @@ export default function AFLArrivees(){ const tomorrowStr = localDateStr(tomorrow); const circulatesOn = (h, d) => { + if (!h || !d) return false; const pad = (n) => String(n).padStart(2, '0'); - const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`; + const targetDay = d.getDay(); + + if (Array.isArray(h.dates) && h.dates.length > 0) { + return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen); + } const customDaysRaw = h.jours_personnalises; - if (!customDaysRaw) return false; - - let customDays = []; - if (Array.isArray(customDaysRaw)) { - customDays = customDaysRaw; - } else if (typeof customDaysRaw === 'string') { - try { - customDays = JSON.parse(customDaysRaw); - } catch (e) { - return false; - } + if (customDaysRaw) { + let customDays = []; + 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 === targetDateCompact); + if (match) return Boolean(match.circule); + } } - if (Array.isArray(customDays)) { - const match = customDays.find(item => item.date === targetDateStr); - if (match) { - return Boolean(match.circule); - } + const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; + const colName = `circule_${dayNames[targetDay]}`; + if (typeof h[colName] !== 'undefined' && h[colName] !== null) { + const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null; + const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null; + if (db && targetDateHyphen < db) return false; + if (df && targetDateHyphen > df) return false; + return Boolean(h[colName]); } - return false; + + if (Array.isArray(h.days) && h.days.length > 0) { + if (!h.days.includes(targetDay)) return false; + if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false; + if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false; + return true; + } + + const singleDate = h.date_depart || h.date || h.service_date; + if (singleDate) { + const sStr = String(singleDate).slice(0, 10); + return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact; + } + + return true; }; // Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_* @@ -1011,6 +1062,10 @@ export default function AFLArrivees(){ let serviceDateStr; if (todayCandidates && todayCandidates.length > 0) { finalList = todayCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999)); + if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) { + const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999)); + finalList = [...finalList, ...sortedTomorrow]; + } serviceDateStr = todayStr; } else { finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999)); @@ -1086,7 +1141,10 @@ export default function AFLArrivees(){ // Helpers pour perturbations (memoized plus haut) - const [pageIndex, setPageIndex] = useState(lockedPageIndex !== null ? lockedPageIndex : 0); + // Gestion du cycle d'alternance des pages : + // Horaires P1 -> Horaires P2 (si > 5 trains) -> Informations -> Sécurité & Sûreté (alternance Img 2 / Img 3) -> Correspondances + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [securityVariant, setSecurityVariant] = useState(2); const [alternateDelay, setAlternateDelay] = useState(false); useEffect(() => { @@ -1095,33 +1153,96 @@ export default function AFLArrivees(){ }, 2000); return () => clearInterval(delayTimer); }, []); - - const getPageBounds = useCallback((pIndex) => { - if (pIndex === 0) return { start: 0, end: 5 }; - const start = 5 + (pIndex - 1) * 8; - return { start, end: start + 8 }; - }, []); - const totalPages = Math.max(1, horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8)); + // Construction des étapes de rotation : + // Les pages Informations, Sécurité 1 et 2, Correspondances ne sont affichées STRICTEMENT que si demandées dans l'URL (?pages=... ou ?infos=1 etc.) + const pagesParam = params.get('pages'); + const selectedPagesList = pagesParam ? pagesParam.split(',').map(s => s.trim().toLowerCase()) : null; + const hasFlag = (key) => params.get(key) === '1' || params.get(key) === 'true'; + + const enableP1 = selectedPagesList + ? (selectedPagesList.includes('p1') || selectedPagesList.includes('horaires') || selectedPagesList.includes('horaires_1') || selectedPagesList.includes('page1')) + : (hasFlag('p1') || hasFlag('horaires') || !pagesParam); + + const enableP2 = selectedPagesList + ? (selectedPagesList.includes('p2') || selectedPagesList.includes('horaires') || selectedPagesList.includes('horaires_2') || selectedPagesList.includes('page2')) + : (hasFlag('p2') || hasFlag('horaires') || !pagesParam); + + const enableInfos = selectedPagesList + ? (selectedPagesList.includes('infos') || selectedPagesList.includes('info') || selectedPagesList.includes('informations') || selectedPagesList.includes('information')) + : (hasFlag('infos') || hasFlag('info') || hasFlag('informations') || hasFlag('information')); + + const enableSecurite1 = selectedPagesList + ? (selectedPagesList.includes('securite1') || selectedPagesList.includes('securite_1') || (selectedPagesList.includes('securite') && !selectedPagesList.includes('securite2'))) + : (hasFlag('securite1') || hasFlag('securite_1') || (hasFlag('securite') && !hasFlag('securite2'))); + + const enableSecurite2 = selectedPagesList + ? (selectedPagesList.includes('securite2') || selectedPagesList.includes('securite_2') || selectedPagesList.includes('vigipirate') || selectedPagesList.includes('3117')) + : (hasFlag('securite2') || hasFlag('securite_2') || hasFlag('vigipirate') || hasFlag('3117')); + + const enableCorresp = selectedPagesList + ? (selectedPagesList.includes('corresp') || selectedPagesList.includes('correspondance') || selectedPagesList.includes('correspondances')) + : (hasFlag('corresp') || hasFlag('correspondance') || hasFlag('correspondances')); + + const cycleSteps = []; + if (enableP1) cycleSteps.push({ type: 'HORAIRES_1' }); + if (enableP2 && horaires.length > 5) cycleSteps.push({ type: 'HORAIRES_2' }); + if (enableInfos) cycleSteps.push({ type: 'INFORMATIONS' }); + if (enableSecurite1) cycleSteps.push({ type: 'SECURITE', variant: 1 }); + if (enableSecurite2) cycleSteps.push({ type: 'SECURITE', variant: 2 }); + if (enableCorresp) cycleSteps.push({ type: 'CORRESPONDANCES' }); + + if (cycleSteps.length === 0) { + cycleSteps.push({ type: 'HORAIRES_1' }); + if (horaires.length > 5) cycleSteps.push({ type: 'HORAIRES_2' }); + } useEffect(() => { - if (lockedPageIndex !== null) { - setPageIndex(Math.min(lockedPageIndex, totalPages - 1)); - return; - } - const pageInterval = setInterval(() => { - setPageIndex((prev) => { - const tot = horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8); - if (tot <= 1) return 0; - return (prev + 1) % tot; - }); - }, 10000); // Change page every 10 seconds - return () => clearInterval(pageInterval); - }, [horaires.length, lockedPageIndex, totalPages]); + if (lockedStep !== null) return; + if (cycleSteps.length <= 1) return; - const { start, end } = getPageBounds(pageIndex); - const displayedHoraires = horaires.slice(start, end); - const isPage1 = pageIndex === 0; + const pageInterval = setInterval(() => { + setCurrentStepIndex((prev) => (prev + 1) % cycleSteps.length); + }, 10000); // 10 secondes par page + + return () => clearInterval(pageInterval); + }, [cycleSteps.length, lockedStep]); + + const activeStep = lockedStep || cycleSteps[currentStepIndex % cycleSteps.length] || { type: 'HORAIRES_1' }; + + const effectiveGareName = gareDisplay || gareParam || (horaires[0]?.gare_arrivee_nom) || (horaires[0]?.gare_arrivee) || (horaires[0]?.nom_gare) || (horaires[0]?.station_name) || ''; + + if (activeStep.type === 'INFORMATIONS') { + return ( + + ); + } + + if (activeStep.type === 'SECURITE') { + return ( + + ); + } + + if (activeStep.type === 'CORRESPONDANCES') { + return ( + + ); + } + + const isPage1 = activeStep.type === 'HORAIRES_1'; + const displayedHoraires = isPage1 ? horaires.slice(0, 5) : horaires.slice(5, 13); + const totalPages = horaires.length <= 5 ? 1 : 2; + const pageIndex = isPage1 ? 0 : 1; return (
@@ -1178,26 +1299,8 @@ export default function AFLArrivees(){ '' ).toString().trim(); - const isCG = rawPlatform.toUpperCase() === 'CG' || - rawPlatform.toUpperCase().includes('CG') || - String(horaire.quai || '').toUpperCase().includes('CG') || - String(horaire.quai_reel || '').toUpperCase().includes('CG') || - String(horaire.quai_prevu || '').toUpperCase().includes('CG') || - String(horaire.platform || '').toUpperCase().includes('CG'); - - const isCarTER = isCG || - op.includes('CAR') || - op.includes('AUTOCAR') || - op.includes('BUS') || - String(horaire.mode || '').toUpperCase().includes('BUS') || - String(horaire.mode || '').toUpperCase().includes('CAR') || - String(horaire.mode_transport || '').toUpperCase().includes('CAR') || - String(horaire.mode_transport || '').toUpperCase().includes('BUS') || - String(horaire.type_train || '').toUpperCase().includes('CAR') || - String(horaire.type_train || '').toUpperCase().includes('BUS') || - String(horaire.libelle_type_train || '').toUpperCase().includes('CAR') || - String(horaire.libelle_type_train || '').toUpperCase().includes('BUS') || - (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero)); + const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero)); + const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR'); let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.time || horaire.heure || ''); const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue'; @@ -1207,7 +1310,7 @@ export default function AFLArrivees(){ const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations)); const isSuppressed = status && status.label && String(status.label).toLowerCase().includes('supprimé'); - let showPlatform = !isSuppressed && Boolean(rawPlatform) && !isCG; + let showPlatform = !isSuppressed && Boolean(rawPlatform); const isDelayed = status && status.label && String(status.label).toLowerCase().includes('retard'); const pillClass = isSuppressed ? 'afl-arr-pill--suppressed' : isDelayed ? 'afl-arr-pill--delayed' : 'afl-arr-pill--ontime'; diff --git a/src/pages/afficheurs/afl/AFLArriveesQuais.js b/src/pages/afficheurs/afl/AFLArriveesQuais.js index c000de4..9b982ac 100644 --- a/src/pages/afficheurs/afl/AFLArriveesQuais.js +++ b/src/pages/afficheurs/afl/AFLArriveesQuais.js @@ -460,31 +460,55 @@ const AFLArriveesQuais = () => { const tomorrowStr = localDateStr(tomorrow); const circulatesOn = (h, d) => { - const pad = (n) => String(n).padStart(2, '0'); - const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + if (!h || !d) return false; + const pad = (n) => String(n).padStart(2, '0'); + const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`; + const targetDay = d.getDay(); - const customDaysRaw = h.jours_personnalises; - if (!customDaysRaw) return false; + if (Array.isArray(h.dates) && h.dates.length > 0) { + return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen); + } - let customDays = []; - if (Array.isArray(customDaysRaw)) { - customDays = customDaysRaw; - } else if (typeof customDaysRaw === 'string') { - try { - customDays = JSON.parse(customDaysRaw); - } catch (e) { - return false; - } - } + const customDaysRaw = h.jours_personnalises; + if (customDaysRaw) { + let customDays = []; + 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 === targetDateCompact); + if (match) return Boolean(match.circule); + } + } - if (Array.isArray(customDays)) { - const match = customDays.find(item => item.date === targetDateStr); - if (match) { - return Boolean(match.circule); - } - } - return false; - }; + const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; + const colName = `circule_${dayNames[targetDay]}`; + if (typeof h[colName] !== 'undefined' && h[colName] !== null) { + const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null; + const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null; + if (db && targetDateHyphen < db) return false; + if (df && targetDateHyphen > df) return false; + return Boolean(h[colName]); + } + + if (Array.isArray(h.days) && h.days.length > 0) { + if (!h.days.includes(targetDay)) return false; + if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false; + if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false; + return true; + } + + const singleDate = h.date_depart || h.date || h.service_date; + if (singleDate) { + const sStr = String(singleDate).slice(0, 10); + return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact; + } + + return true; + }; const parseDateOnly = (v) => { if (!v) return null; @@ -608,6 +632,10 @@ const AFLArriveesQuais = () => { let serviceDateStr = ''; if (todayCandidates.length > 0) { finalList = todayCandidates.slice().sort((a, b) => a._depHM - b._depHM); + if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) { + const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999)); + finalList = [...finalList, ...sortedTomorrow]; + } serviceDateStr = todayStr; } else { finalList = tomorrowCandidates.slice().sort((a, b) => a._depHM - b._depHM); diff --git a/src/pages/afficheurs/afl/AFLDeparts.js b/src/pages/afficheurs/afl/AFLDeparts.js index 795540a..7911fe4 100644 --- a/src/pages/afficheurs/afl/AFLDeparts.js +++ b/src/pages/afficheurs/afl/AFLDeparts.js @@ -7,6 +7,9 @@ import useAnnonceSonoreRealtime from '../../../hooks/useAnnonceSonoreRealtime'; import useAutoAnnonce from '../../../hooks/useAutoAnnonce'; import { getSourceFromPath } from '../../../services/railwayService'; import { getSncfTimetable } from '../../../services/sncfApi'; +import AFLSecurityPage from './components/AFLSecurityPage'; +import AFLCorrespondancesPage from './components/AFLCorrespondancesPage'; +import AFLInformationsPage from './components/AFLInformationsPage'; // Helper utilitaire : format HHhMM sans secondes (ex: 06h44) const formatTimeNoSeconds = (v) => { @@ -135,11 +138,35 @@ export default function AFLDeparts(){ // gérer les +, décoder et remplacer les tirets par des espaces const gareDisplay = gareParam ? decodeURIComponent(String(gareParam).replace(/\+/g, ' ')).replace(/-/g, ' ') : null; - // Support pour ?page=1, ?page=2 pour verrouiller l'affichage sur une page spécifique - const pageParam = params.get('page'); - const lockedPageIndex = pageParam !== null && !isNaN(parseInt(pageParam, 10)) - ? Math.max(0, parseInt(pageParam, 10) - 1) - : null; + // Support pour verrouiller l'affichage sur une page spécifique via l'URL (?page=...) + // ?page=1 -> Horaires Page 1 + // ?page=2 -> Horaires Page 2 + // ?page=3 | ?page=info | ?page=informations -> Informations + // ?page=4 | ?page=securite | ?page=securite1 -> Sécurité (Image 2) + // ?page=securite2 -> Sécurité (Image 3) + // ?page=5 | ?page=correspondances -> Correspondances + const rawPageParam = params.get('page') ? String(params.get('page')).toLowerCase().trim() : null; + + const parseLockedStep = (param) => { + if (!param) return null; + if (param === '1' || param === 'p1' || param === 'horaires1' || param === 'horaires_1') return { type: 'HORAIRES_1' }; + if (param === '2' || param === 'p2' || param === 'horaires2' || param === 'horaires_2') return { type: 'HORAIRES_2' }; + if (param === '3' || param === 'info' || param === 'information' || param === 'informations' || param === 'infos') return { type: 'INFORMATIONS' }; + if (param === '4' || param === 'securite' || param === 'security' || param === 'safety' || param === 'securite1' || param === 'securite_1') return { type: 'SECURITE', variant: 1 }; + if (param === 'securite2' || param === 'securite_2' || param === 'vigipirate' || param === '3117') return { type: 'SECURITE', variant: 2 }; + if (param === '5' || param === 'correspondance' || param === 'correspondances' || param === 'corresp') return { type: 'CORRESPONDANCES' }; + const n = parseInt(param, 10); + if (!isNaN(n)) { + if (n === 1) return { type: 'HORAIRES_1' }; + if (n === 2) return { type: 'HORAIRES_2' }; + if (n === 3) return { type: 'INFORMATIONS' }; + if (n === 4) return { type: 'SECURITE', variant: 1 }; + if (n >= 5) return { type: 'CORRESPONDANCES' }; + } + return null; + }; + + const lockedStep = parseLockedStep(rawPageParam); // debug flag activable via ?debug=1 or ?debug=true const debugEnabled = (params.get('debug') === '1' || String(params.get('debug')).toLowerCase() === 'true'); @@ -164,7 +191,7 @@ export default function AFLDeparts(){ // type de la gare affichée (valeurs attendues: 'interurbaine', 'ville', ...) // eslint-disable-next-line no-unused-vars const [stationType, setStationType] = useState(null); - const [, setTravaux] = useState([]); + const [travaux, setTravaux] = useState([]); const [travauxSubstitutions, setTravauxSubstitutions] = useState([]); const [, setLoading] = useState(false); const [, setError] = useState(null); @@ -695,7 +722,7 @@ export default function AFLDeparts(){ causeDetails: t.cause_retard || null } })); - setHoraires(formatted.slice(0, 10)); + setHoraires(formatted); setAllHoraires(formatted); } } catch (e) { @@ -886,30 +913,54 @@ export default function AFLDeparts(){ const tomorrowStr = localDateStr(tomorrow); const circulatesOn = (h, d) => { + if (!h || !d) return false; const pad = (n) => String(n).padStart(2, '0'); - const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`; + const targetDay = d.getDay(); + + if (Array.isArray(h.dates) && h.dates.length > 0) { + return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen); + } const customDaysRaw = h.jours_personnalises; - if (!customDaysRaw) return false; - - let customDays = []; - if (Array.isArray(customDaysRaw)) { - customDays = customDaysRaw; - } else if (typeof customDaysRaw === 'string') { - try { - customDays = JSON.parse(customDaysRaw); - } catch (e) { - return false; - } + if (customDaysRaw) { + let customDays = []; + 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 === targetDateCompact); + if (match) return Boolean(match.circule); + } } - if (Array.isArray(customDays)) { - const match = customDays.find(item => item.date === targetDateStr); - if (match) { - return Boolean(match.circule); - } + const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; + const colName = `circule_${dayNames[targetDay]}`; + if (typeof h[colName] !== 'undefined' && h[colName] !== null) { + const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null; + const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null; + if (db && targetDateHyphen < db) return false; + if (df && targetDateHyphen > df) return false; + return Boolean(h[colName]); } - return false; + + if (Array.isArray(h.days) && h.days.length > 0) { + if (!h.days.includes(targetDay)) return false; + if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false; + if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false; + return true; + } + + const singleDate = h.date_depart || h.date || h.service_date; + if (singleDate) { + const sStr = String(singleDate).slice(0, 10); + return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact; + } + + return true; }; // Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_* @@ -1036,6 +1087,10 @@ export default function AFLDeparts(){ let serviceDateStr; if (todayCandidates && todayCandidates.length > 0) { finalList = todayCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999)); + if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) { + const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999)); + finalList = [...finalList, ...sortedTomorrow]; + } serviceDateStr = todayStr; } else { finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999)); @@ -1111,7 +1166,10 @@ export default function AFLDeparts(){ // Helpers pour perturbations (memoized plus haut) - const [pageIndex, setPageIndex] = useState(lockedPageIndex !== null ? lockedPageIndex : 0); + // Gestion du cycle d'alternance des pages : + // Horaires P1 -> Horaires P2 (si > 5 trains) -> Informations -> Sécurité & Sûreté (alternance Img 2 / Img 3) -> Correspondances + const [currentStepIndex, setCurrentStepIndex] = useState(0); + const [securityVariant, setSecurityVariant] = useState(2); const [alternateDelay, setAlternateDelay] = useState(false); useEffect(() => { @@ -1120,33 +1178,96 @@ export default function AFLDeparts(){ }, 2000); return () => clearInterval(delayTimer); }, []); - - const getPageBounds = useCallback((pIndex) => { - if (pIndex === 0) return { start: 0, end: 5 }; - const start = 5 + (pIndex - 1) * 8; - return { start, end: start + 8 }; - }, []); - const totalPages = Math.max(1, horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8)); + // Construction des étapes de rotation : + // Les pages Informations, Sécurité 1 et 2, Correspondances ne sont affichées STRICTEMENT que si demandées dans l'URL (?pages=... ou ?infos=1 etc.) + const pagesParam = params.get('pages'); + const selectedPagesList = pagesParam ? pagesParam.split(',').map(s => s.trim().toLowerCase()) : null; + const hasFlag = (key) => params.get(key) === '1' || params.get(key) === 'true'; + + const enableP1 = selectedPagesList + ? (selectedPagesList.includes('p1') || selectedPagesList.includes('horaires') || selectedPagesList.includes('horaires_1') || selectedPagesList.includes('page1')) + : (hasFlag('p1') || hasFlag('horaires') || !pagesParam); + + const enableP2 = selectedPagesList + ? (selectedPagesList.includes('p2') || selectedPagesList.includes('horaires') || selectedPagesList.includes('horaires_2') || selectedPagesList.includes('page2')) + : (hasFlag('p2') || hasFlag('horaires') || !pagesParam); + + const enableInfos = selectedPagesList + ? (selectedPagesList.includes('infos') || selectedPagesList.includes('info') || selectedPagesList.includes('informations') || selectedPagesList.includes('information')) + : (hasFlag('infos') || hasFlag('info') || hasFlag('informations') || hasFlag('information')); + + const enableSecurite1 = selectedPagesList + ? (selectedPagesList.includes('securite1') || selectedPagesList.includes('securite_1') || (selectedPagesList.includes('securite') && !selectedPagesList.includes('securite2'))) + : (hasFlag('securite1') || hasFlag('securite_1') || (hasFlag('securite') && !hasFlag('securite2'))); + + const enableSecurite2 = selectedPagesList + ? (selectedPagesList.includes('securite2') || selectedPagesList.includes('securite_2') || selectedPagesList.includes('vigipirate') || selectedPagesList.includes('3117')) + : (hasFlag('securite2') || hasFlag('securite_2') || hasFlag('vigipirate') || hasFlag('3117')); + + const enableCorresp = selectedPagesList + ? (selectedPagesList.includes('corresp') || selectedPagesList.includes('correspondance') || selectedPagesList.includes('correspondances')) + : (hasFlag('corresp') || hasFlag('correspondance') || hasFlag('correspondances')); + + const cycleSteps = []; + if (enableP1) cycleSteps.push({ type: 'HORAIRES_1' }); + if (enableP2 && horaires.length > 5) cycleSteps.push({ type: 'HORAIRES_2' }); + if (enableInfos) cycleSteps.push({ type: 'INFORMATIONS' }); + if (enableSecurite1) cycleSteps.push({ type: 'SECURITE', variant: 1 }); + if (enableSecurite2) cycleSteps.push({ type: 'SECURITE', variant: 2 }); + if (enableCorresp) cycleSteps.push({ type: 'CORRESPONDANCES' }); + + if (cycleSteps.length === 0) { + cycleSteps.push({ type: 'HORAIRES_1' }); + if (horaires.length > 5) cycleSteps.push({ type: 'HORAIRES_2' }); + } useEffect(() => { - if (lockedPageIndex !== null) { - setPageIndex(Math.min(lockedPageIndex, totalPages - 1)); - return; - } - const pageInterval = setInterval(() => { - setPageIndex((prev) => { - const tot = horaires.length <= 5 ? 1 : 1 + Math.ceil((horaires.length - 5) / 8); - if (tot <= 1) return 0; - return (prev + 1) % tot; - }); - }, 10000); // Change page every 10 seconds - return () => clearInterval(pageInterval); - }, [horaires.length, lockedPageIndex, totalPages]); + if (lockedStep !== null) return; + if (cycleSteps.length <= 1) return; - const { start, end } = getPageBounds(pageIndex); - const displayedHoraires = horaires.slice(start, end); - const isPage1 = pageIndex === 0; + const pageInterval = setInterval(() => { + setCurrentStepIndex((prev) => (prev + 1) % cycleSteps.length); + }, 10000); // 10 secondes par page + + return () => clearInterval(pageInterval); + }, [cycleSteps.length, lockedStep]); + + const activeStep = lockedStep || cycleSteps[currentStepIndex % cycleSteps.length] || { type: 'HORAIRES_1' }; + + const effectiveGareName = gareDisplay || gareParam || (horaires[0]?.gare_depart_nom) || (horaires[0]?.gare_depart) || (horaires[0]?.nom_gare) || (horaires[0]?.station_name) || ''; + + if (activeStep.type === 'INFORMATIONS') { + return ( + + ); + } + + if (activeStep.type === 'SECURITE') { + return ( + + ); + } + + if (activeStep.type === 'CORRESPONDANCES') { + return ( + + ); + } + + const isPage1 = activeStep.type === 'HORAIRES_1'; + const displayedHoraires = isPage1 ? horaires.slice(0, 5) : horaires.slice(5, 13); + const totalPages = horaires.length <= 5 ? 1 : 2; + const pageIndex = isPage1 ? 0 : 1; return (
@@ -1203,26 +1324,8 @@ export default function AFLDeparts(){ '' ).toString().trim(); - const isCG = rawPlatform.toUpperCase() === 'CG' || - rawPlatform.toUpperCase().includes('CG') || - String(horaire.quai || '').toUpperCase().includes('CG') || - String(horaire.quai_reel || '').toUpperCase().includes('CG') || - String(horaire.quai_prevu || '').toUpperCase().includes('CG') || - String(horaire.platform || '').toUpperCase().includes('CG'); - - const isCarTER = isCG || - op.includes('CAR') || - op.includes('AUTOCAR') || - op.includes('BUS') || - String(horaire.mode || '').toUpperCase().includes('BUS') || - String(horaire.mode || '').toUpperCase().includes('CAR') || - String(horaire.mode_transport || '').toUpperCase().includes('CAR') || - String(horaire.mode_transport || '').toUpperCase().includes('BUS') || - String(horaire.type_train || '').toUpperCase().includes('CAR') || - String(horaire.type_train || '').toUpperCase().includes('BUS') || - String(horaire.libelle_type_train || '').toUpperCase().includes('CAR') || - String(horaire.libelle_type_train || '').toUpperCase().includes('BUS') || - (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero)); + const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero)); + const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR'); let time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.time || horaire.heure || ''); const destination = horaire.gare_arrivee_nom || ''; @@ -1232,7 +1335,7 @@ export default function AFLDeparts(){ const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations)); const isSuppressed = status && status.label && String(status.label).toLowerCase().includes('supprimé'); - let showPlatform = !isSuppressed && Boolean(rawPlatform) && !isCG; + let showPlatform = !isSuppressed && Boolean(rawPlatform); const isDelayed = status && status.label && String(status.label).toLowerCase().includes('retard'); const pillClass = isSuppressed ? 'afl-pill--suppressed' : isDelayed ? 'afl-pill--delayed' : 'afl-pill--ontime'; diff --git a/src/pages/afficheurs/afl/AFLDepartsQuais.js b/src/pages/afficheurs/afl/AFLDepartsQuais.js index d13b011..dd7bb53 100644 --- a/src/pages/afficheurs/afl/AFLDepartsQuais.js +++ b/src/pages/afficheurs/afl/AFLDepartsQuais.js @@ -530,31 +530,55 @@ const AFLDepartsQuais = () => { const tomorrowStr = localDateStr(tomorrow); const circulatesOn = (h, d) => { - const pad = (n) => String(n).padStart(2, '0'); - const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + if (!h || !d) return false; + const pad = (n) => String(n).padStart(2, '0'); + const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`; + const targetDay = d.getDay(); - const customDaysRaw = h.jours_personnalises; - if (!customDaysRaw) return false; + if (Array.isArray(h.dates) && h.dates.length > 0) { + return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen); + } - let customDays = []; - if (Array.isArray(customDaysRaw)) { - customDays = customDaysRaw; - } else if (typeof customDaysRaw === 'string') { - try { - customDays = JSON.parse(customDaysRaw); - } catch (e) { - return false; - } - } + const customDaysRaw = h.jours_personnalises; + if (customDaysRaw) { + let customDays = []; + 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 === targetDateCompact); + if (match) return Boolean(match.circule); + } + } - if (Array.isArray(customDays)) { - const match = customDays.find(item => item.date === targetDateStr); - if (match) { - return Boolean(match.circule); - } - } - return false; - }; + const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; + const colName = `circule_${dayNames[targetDay]}`; + if (typeof h[colName] !== 'undefined' && h[colName] !== null) { + const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null; + const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null; + if (db && targetDateHyphen < db) return false; + if (df && targetDateHyphen > df) return false; + return Boolean(h[colName]); + } + + if (Array.isArray(h.days) && h.days.length > 0) { + if (!h.days.includes(targetDay)) return false; + if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false; + if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false; + return true; + } + + const singleDate = h.date_depart || h.date || h.service_date; + if (singleDate) { + const sStr = String(singleDate).slice(0, 10); + return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact; + } + + return true; + }; const parseDateOnly = (v) => { if (!v) return null; @@ -682,6 +706,10 @@ const AFLDepartsQuais = () => { let serviceDateStr = ''; if (todayCandidates.length > 0) { finalList = todayCandidates.slice().sort((a, b) => a._depHM - b._depHM); + if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) { + const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999)); + finalList = [...finalList, ...sortedTomorrow]; + } serviceDateStr = todayStr; } else { finalList = tomorrowCandidates.slice().sort((a, b) => a._depHM - b._depHM); diff --git a/src/pages/afficheurs/afl/components/AFLCorrespondancesPage.css b/src/pages/afficheurs/afl/components/AFLCorrespondancesPage.css new file mode 100644 index 0000000..2191811 --- /dev/null +++ b/src/pages/afficheurs/afl/components/AFLCorrespondancesPage.css @@ -0,0 +1,201 @@ +/* AFL CORRESPONDANCES PAGE - DESIGN STRICT CONFORME A L'IMAGE 1 */ + +.afl-corresp-page { + font-family: 'Achemine', 'Segoe UI', Roboto, sans-serif !important; + background-color: #1f2125 !important; + color: #ffffff !important; + width: 100% !important; + height: 100% !important; + overflow: hidden !important; + display: flex !important; + flex-direction: column !important; + position: absolute !important; + top: 0 !important; + left: 0 !important; + user-select: none !important; + box-sizing: border-box !important; +} + +.afl-corresp-page * { + box-sizing: border-box !important; +} + +/* ========================================================================== + EN-TÊTE GRIS ARDOISE (108px) + ========================================================================== */ +.afl-corresp-header { + height: 108px !important; + min-height: 108px !important; + max-height: 108px !important; + background-color: #34373d !important; + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + padding: 0 48px !important; + border-bottom: 2px solid #4a4e56 !important; +} + +.afl-corresp-header-left { + display: flex !important; + align-items: center !important; + gap: 22px !important; +} + +.afl-corresp-header-title { + font-size: 54px !important; + font-weight: 800 !important; + color: #ffffff !important; + letter-spacing: -0.5px !important; + line-height: 1 !important; +} + +.afl-corresp-header-right { + display: flex !important; + align-items: center !important; +} + +.afl-corresp-clock { + font-size: 56px !important; + font-weight: 800 !important; + color: #ffffff !important; + line-height: 1 !important; + letter-spacing: -0.96px !important; +} + +/* ========================================================================== + LISTE DES LIGNES EN CORRESPONDANCE + ========================================================================== */ +.afl-corresp-list { + flex: 1 !important; + display: flex !important; + flex-direction: column !important; + padding: 0 !important; + margin: 0 !important; + background-color: #1f2125 !important; +} + +.afl-corresp-group { + display: flex !important; + flex-direction: column !important; + width: 100% !important; +} + +.afl-corresp-row { + height: 108px !important; + min-height: 108px !important; + max-height: 108px !important; + display: flex !important; + align-items: center !important; + width: 100% !important; + padding: 0 48px !important; + border-bottom: 1.5px solid #3c4048 !important; +} + +/* Colonne 1 : Bus/Car/Tram + Numéro de ligne */ +.afl-corresp-col-line { + display: flex !important; + align-items: center !important; + gap: 14px !important; + width: 180px !important; + min-width: 180px !important; +} + +.afl-corresp-icon-box { + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 58px !important; + height: 58px !important; + border: 2px solid rgba(255, 255, 255, 0.4) !important; + border-radius: 8px !important; + background: rgba(255, 255, 255, 0.05) !important; + flex-shrink: 0 !important; +} + +.afl-corresp-line-badge { + background-color: #4a4e57 !important; + color: #ffffff !important; + font-size: 32px !important; + font-weight: 900 !important; + padding: 6px 14px !important; + border-radius: 8px !important; + line-height: 1 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3) !important; + min-width: 65px !important; + text-align: center !important; +} + +.afl-corresp-line-spacer { + width: 180px !important; + min-width: 180px !important; +} + +/* Colonne 2 : Arrêt - Destination */ +.afl-corresp-col-dest { + flex: 1 !important; + overflow: hidden !important; + padding-right: 30px !important; + white-space: nowrap !important; + text-overflow: ellipsis !important; +} + +.afl-corresp-dest-text { + font-size: 48px !important; + font-weight: 700 !important; + color: #ffffff !important; + letter-spacing: -0.5px !important; + line-height: 1 !important; +} + +/* Colonne 3 : 2 Boîtes des minutes d'attente (Compatibles XXhXX min) */ +.afl-corresp-col-times { + display: flex !important; + align-items: center !important; + gap: 20px !important; + flex-shrink: 0 !important; +} + +.afl-corresp-time-box { + background-color: #d6dbe2 !important; + color: #111111 !important; + border-radius: 10px !important; + min-width: 155px !important; + height: 68px !important; + padding: 0 16px !important; + display: flex !important; + align-items: baseline !important; + justify-content: center !important; + gap: 4px !important; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.35) !important; +} + +.afl-corresp-time-val { + font-size: 44px !important; + font-weight: 900 !important; + line-height: 1 !important; + letter-spacing: -1px !important; +} + +.afl-corresp-time-unit { + font-size: 24px !important; + font-weight: 600 !important; + line-height: 1 !important; + opacity: 0.9 !important; + margin-left: 2px !important; +} + +/* Message si liste vide */ +.afl-corresp-empty { + flex: 1 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + color: #cccccc !important; + font-size: 32px !important; + font-weight: 500 !important; + padding: 40px !important; + text-align: center !important; +} diff --git a/src/pages/afficheurs/afl/components/AFLCorrespondancesPage.js b/src/pages/afficheurs/afl/components/AFLCorrespondancesPage.js new file mode 100644 index 0000000..333298d --- /dev/null +++ b/src/pages/afficheurs/afl/components/AFLCorrespondancesPage.js @@ -0,0 +1,214 @@ +import React, { useState, useEffect } from 'react'; +import './AFLCorrespondancesPage.css'; +import { getCorrespondancesForStation, computeLiveWaitTimes, formatWaitTimeDisplay } from '../../../../services/correspondancesService'; + +// Horloge pour l'en-tête de correspondances +function CorrespondancesClock({ now }) { + const hh = String(now.getHours()).padStart(2, '0'); + const mm = String(now.getMinutes()).padStart(2, '0'); + + return ( +
+ {hh}:{mm} +
+ ); +} + +// Icône de Pôle d'échange / Gare Correspondances (bâtiment + trajet en pointillés) +const StationTransferIcon = ({ size = '52px', color = '#f5c518' }) => ( + + {/* Bâtiment gare / Pôle d'échange */} + + + + + + {/* Trajet pointillé de correspondance */} + + + +); + +// Icône de Transport Multimodal (Bus, Car, Tram) +const TransitCorrespIcon = ({ type = 'bus', size = '56px', color = '#ffffff' }) => ( +
+ {type === 'tram' ? ( + + + + + + + + + + ) : type === 'car' ? ( + + + + + + + + + + ) : ( + + + + + + + + + )} +
+); + +export default function AFLCorrespondancesPage({ gareName = '' }) { + const [correspondances, setCorrespondances] = useState([]); + const [, setLoading] = useState(true); + const [currentTime, setCurrentTime] = useState(new Date()); + + // Horloge en direct pour synchronisation exacte des décomptes + useEffect(() => { + const clockId = setInterval(() => setCurrentTime(new Date()), 1000); + return () => clearInterval(clockId); + }, []); + + // Charger les correspondances aux alentours de la gare affichée + useEffect(() => { + let mounted = true; + + async function loadData() { + try { + const list = await getCorrespondancesForStation(gareName); + if (mounted) { + setCorrespondances(list); + setLoading(false); + } + } catch (err) { + console.warn('[AFLCorrespondances] Erreur chargement correspondances:', err); + if (mounted) setLoading(false); + } + } + + loadData(); + + // Actualiser les correspondances toutes les 30 secondes + const refreshTimer = setInterval(loadData, 30000); + return () => { + mounted = false; + clearInterval(refreshTimer); + }; + }, [gareName]); + + // Si aucune correspondance n'est disponible + if (!correspondances || correspondances.length === 0) { + return ( +
+
+
+ +
Correspondances
+
+
+ +
+
+
+ Aucune correspondance directe actuellement signalée dans cette gare. +
+
+ ); + } + + return ( +
+ {/* EN-TÊTE GRIS ARDOISE */} +
+
+ +
Correspondances
+
+ +
+ +
+
+ + {/* LISTE DES CORRESPONDANCES (Strictement conforme à l'Image 1) */} +
+ {correspondances.map((cGroup) => ( +
+ {cGroup.destinations.map((dest, dIdx) => { + const isFirstInGroup = dIdx === 0; + + // Calcul en direct à la seconde près des 2 prochains départs (comme Image 1) + const times = computeLiveWaitTimes(dest, currentTime).slice(0, 2); + + return ( +
+ {/* COLONNE GAUCHE : Picto Transport et Badge Ligne UNIQUEMENT sur la 1ère destination du groupe */} +
+ {isFirstInGroup ? ( + <> + +
+ {cGroup.line} +
+ + ) : ( +
+ )} +
+ + {/* COLONNE CENTRE : Arrêt - Destination */} +
+ {dest.name} +
+ + {/* COLONNE DROITE : 2 Boîtes Minutes d'attente compatibles XXhXX min */} +
+ {times.map((wait, wIdx) => { + const formatted = formatWaitTimeDisplay(wait); + return ( +
+ {formatted.val} + {formatted.unit && {formatted.unit}} +
+ ); + })} +
+
+ ); + })} +
+ ))} +
+
+ ); +} diff --git a/src/pages/afficheurs/afl/components/AFLInformationsPage.css b/src/pages/afficheurs/afl/components/AFLInformationsPage.css new file mode 100644 index 0000000..e11fc26 --- /dev/null +++ b/src/pages/afficheurs/afl/components/AFLInformationsPage.css @@ -0,0 +1,242 @@ +/* AFL INFORMATIONS PAGE - DESIGN AUX COULEURS AFL */ + +.afl-info-page { + font-family: 'Achemine', 'Segoe UI', Roboto, sans-serif !important; + color: #ffffff !important; + width: 100% !important; + height: 100% !important; + overflow: hidden !important; + display: flex !important; + flex-direction: column !important; + position: absolute !important; + top: 0 !important; + left: 0 !important; + user-select: none !important; + box-sizing: border-box !important; +} + +.afl-info-page * { + box-sizing: border-box !important; +} + +/* Thème Départs (Bleu) */ +.afl-info--departs { + background-color: #002d62 !important; +} + +.afl-info--departs .afl-info-header { + background-color: #005fa9 !important; + border-bottom: 2px solid #ffffff !important; +} + +/* Thème Arrivées (Vert) */ +.afl-info--arrivees { + background-color: #173e31 !important; +} + +.afl-info--arrivees .afl-info-header { + background-color: #337862 !important; + border-bottom: 2px solid #ffffff !important; +} + +/* ========================================================================== + EN-TÊTE AFL (108px) + ========================================================================== */ +.afl-info-header { + height: 108px !important; + min-height: 108px !important; + max-height: 108px !important; + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + padding: 0 48px !important; +} + +.afl-info-header-left { + display: flex !important; + align-items: center !important; + gap: 22px !important; +} + +.afl-info-header-title { + font-size: 54px !important; + font-weight: 800 !important; + color: #ffffff !important; + letter-spacing: -0.5px !important; + line-height: 1 !important; +} + +.afl-info-header-right { + display: flex !important; + align-items: center !important; +} + +.afl-info-clock { + font-size: 56px !important; + font-weight: 800 !important; + color: #ffffff !important; + line-height: 1 !important; + letter-spacing: -0.96px !important; +} + +/* ========================================================================== + CONTENU PRINCIPAL + ========================================================================== */ +.afl-info-content { + flex: 1 !important; + display: flex !important; + flex-direction: column !important; + justify-content: center !important; + padding: 36px 60px !important; + overflow: hidden !important; +} + +/* Grille des cartes en cas de perturbations ou travaux */ +.afl-info-cards-container { + display: flex !important; + flex-direction: column !important; + gap: 24px !important; + height: 100% !important; + justify-content: center !important; +} + +.afl-info-card { + background: rgba(255, 255, 255, 0.08) !important; + border-radius: 16px !important; + padding: 24px 32px !important; + border: 1.5px solid rgba(255, 255, 255, 0.2) !important; + backdrop-filter: blur(8px) !important; +} + +.afl-info-card--perturbation { + border-left: 10px solid #f7a81b !important; +} + +.afl-info-card--travaux { + border-left: 10px solid #3cd070 !important; +} + +.afl-info-card-header { + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + margin-bottom: 16px !important; +} + +.afl-info-badge { + display: flex !important; + align-items: center !important; + gap: 10px !important; + font-size: 26px !important; + font-weight: 800 !important; + text-transform: uppercase !important; + letter-spacing: 0.8px !important; +} + +.afl-info-badge--alert { + color: #f7a81b !important; +} + +.afl-info-badge--travaux { + color: #3cd070 !important; +} + +.afl-info-station-name { + font-size: 32px !important; + font-weight: 700 !important; + color: #ffffff !important; + opacity: 0.9 !important; +} + +.afl-info-card-body { + display: flex !important; + flex-direction: column !important; + gap: 14px !important; +} + +.afl-info-perturb-item, +.afl-info-travaux-item { + display: flex !important; + flex-direction: column !important; + gap: 4px !important; +} + +.afl-info-perturb-title, +.afl-info-travaux-title { + font-size: 38px !important; + font-weight: 700 !important; + color: #ffffff !important; +} + +.afl-info-perturb-desc, +.afl-info-travaux-desc { + font-size: 26px !important; + font-weight: 400 !important; + color: #e0e6ed !important; + line-height: 1.3 !important; +} + +/* ========================================================================== + AFFICHAGE NOMINAL (3 CARTES DE SERVICES & CONSEILS) + ========================================================================== */ +.afl-info-nominal-container { + display: flex !important; + flex-direction: column !important; + justify-content: space-evenly !important; + height: 100% !important; + gap: 18px !important; +} + +.afl-info-nominal-card { + display: flex !important; + align-items: center !important; + gap: 36px !important; + background: rgba(255, 255, 255, 0.08) !important; + border: 1.5px solid rgba(255, 255, 255, 0.16) !important; + border-radius: 18px !important; + padding: 24px 36px !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2) !important; +} + +.afl-info-nominal-icon-wrap { + width: 96px !important; + height: 96px !important; + min-width: 96px !important; + border-radius: 20px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25) !important; +} + +.afl-info-nominal-icon--green { + background: linear-gradient(135deg, #1fa357, #13773d) !important; +} + +.afl-info-nominal-icon--blue { + background: linear-gradient(135deg, #0088ff, #0055cc) !important; +} + +.afl-info-nominal-icon--yellow { + background: linear-gradient(135deg, #e69d00, #b37400) !important; +} + +.afl-info-nominal-text { + display: flex !important; + flex-direction: column !important; + gap: 6px !important; +} + +.afl-info-nominal-title { + font-size: 40px !important; + font-weight: 800 !important; + color: #ffffff !important; + letter-spacing: -0.3px !important; +} + +.afl-info-nominal-subtitle { + font-size: 27px !important; + font-weight: 400 !important; + color: #dbe4ef !important; + line-height: 1.25 !important; +} diff --git a/src/pages/afficheurs/afl/components/AFLInformationsPage.js b/src/pages/afficheurs/afl/components/AFLInformationsPage.js new file mode 100644 index 0000000..5b8669d --- /dev/null +++ b/src/pages/afficheurs/afl/components/AFLInformationsPage.js @@ -0,0 +1,185 @@ +import React, { useState, useEffect } from 'react'; +import './AFLInformationsPage.css'; + +function InfoClock() { + 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'); + + return ( +
+ {hh}:{mm} +
+ ); +} + +export default function AFLInformationsPage({ + mode = 'departs', + gareName = '', + perturbations = [], + travaux = [] +}) { + const isDeparts = mode === 'departs'; + const themeClass = isDeparts ? 'afl-info--departs' : 'afl-info--arrivees'; + + // Filtrer les perturbations et travaux pertinents + const activePerturbs = (perturbations || []).filter(p => { + const t = (p.type_perturbation || p.type || '').toString().toLowerCase(); + return Boolean(t); + }); + + const activeTravaux = (travaux || []).slice(0, 2); + + const hasPerturbations = activePerturbs.length > 0; + const hasTravaux = activeTravaux.length > 0; + + return ( +
+ {/* EN-TÊTE AUX COULEURS AFL */} +
+
+ {/* Icône Information Blanche */} + + + + + +
Informations
+
+ +
+ +
+
+ + {/* CONTENU PRINCIPAL DE LA PAGE INFORMATIONS */} +
+ {hasPerturbations || hasTravaux ? ( +
+ {/* CARTE PERTURBATION */} + {hasPerturbations && ( +
+
+
+ + + + + + Information Trafic +
+
{gareName || 'Votre Gare'}
+
+ +
+ {activePerturbs.slice(0, 2).map((p, idx) => ( +
+
+ {p.ligne_nom || p.ligne || p.type_perturbation || 'Perturbation en cours'} +
+
+ {p.message || p.description || p.detail || p.cause || 'Des retards ou adaptations de circulation peuvent intervenir sur vos trajets. Veuillez consulter les annonces en gare.'} +
+
+ ))} +
+
+ )} + + {/* CARTE TRAVAUX */} + {hasTravaux && ( +
+
+
+ + + + Travaux de maintenance +
+
+ +
+ {activeTravaux.map((t, idx) => ( +
+
{t.nom || t.intitule || 'Modernisation du réseau'}
+
+ {t.description || t.motif || 'Des substitutions par autocar peuvent être mises en place pendant la période de travaux.'} +
+
+ ))} +
+
+ )} +
+ ) : ( + /* ÉCRAN NOMINAL : INFORMATIONS VOYAGEURS & TRAFIC FLUIDE */ +
+ {/* CARTE 1 : STATUT TRAFIC NORMAL */} +
+
+ + + + +
+
+
Trafic normal
+
+ La circulation des trains est fluide sur l'ensemble de votre ligne. +
+
+
+ + {/* CARTE 2 : CONSEILS VOYAGEURS ET APPLICATION */} +
+
+ + + + +
+
+
SNCF Connect & Services
+
+ Retrouvez vos horaires en direct, achetez vos billets et suivez votre train en temps réel sur l'application SNCF Connect. +
+
+
+ + {/* CARTE 3 : CONSEILS DE SÉCURITÉ EN GARE */} +
+
+ + + + + +
+
+
Étiquetage de vos bagages
+
+ Pensez à apposer une étiquette visible avec vos nom, prénom et numéro de téléphone sur tous vos bagages. +
+
+
+
+ )} +
+
+ ); +} diff --git a/src/pages/afficheurs/afl/components/AFLSecurityPage.css b/src/pages/afficheurs/afl/components/AFLSecurityPage.css new file mode 100644 index 0000000..7a1fb14 --- /dev/null +++ b/src/pages/afficheurs/afl/components/AFLSecurityPage.css @@ -0,0 +1,362 @@ +/* AFL SECURITY & SAFETY PAGE - CONFORME STRICTEMENT AUX MAQUETTES IMAGES 2 ET 3 */ + +.afl-sec-page { + font-family: 'Achemine', 'Segoe UI', Roboto, sans-serif !important; + width: 100% !important; + height: 100% !important; + overflow: hidden !important; + display: flex !important; + flex-direction: column !important; + position: absolute !important; + top: 0 !important; + left: 0 !important; + user-select: none !important; + box-sizing: border-box !important; +} + +.afl-sec-page * { + box-sizing: border-box !important; +} + +/* ========================================================================== + EN-TÊTE BLANCHE COMMUNE (108px) + ========================================================================== */ +.afl-sec-header { + height: 108px !important; + min-height: 108px !important; + max-height: 108px !important; + background-color: #ffffff !important; + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + padding: 0 48px !important; + border-bottom: 2px solid #e0e0e0 !important; +} + +.afl-sec-header-left { + display: flex !important; + align-items: center !important; + gap: 20px !important; +} + +.afl-sec-header-title { + font-size: 54px !important; + font-weight: 800 !important; + color: #111111 !important; + letter-spacing: -0.5px !important; + line-height: 1 !important; +} + +.afl-sec-header-right { + display: flex !important; + align-items: center !important; +} + +.afl-sec-clock { + font-size: 56px !important; + font-weight: 800 !important; + color: #111111 !important; + line-height: 1 !important; + letter-spacing: -0.96px !important; +} + +/* ========================================================================== + CONTENU VARIANT 1 : ORANGE (Traversée des voies - Image 2) + ========================================================================== */ +.afl-sec-content--orange { + flex: 1 !important; + background-color: #d63e18 !important; + display: flex !important; + flex-direction: column !important; + overflow: hidden !important; +} + +/* Ruban rayé sécurité orange / blanc */ +.afl-sec-hazard-ribbon { + height: 38px !important; + min-height: 38px !important; + width: 100% !important; + background: repeating-linear-gradient( + 135deg, + #ffffff 0px, + #ffffff 24px, + #d63e18 24px, + #d63e18 48px + ) !important; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2) !important; +} + +.afl-sec-orange-body { + flex: 1 !important; + display: flex !important; + flex-direction: column !important; + justify-content: space-evenly !important; + padding: 30px 60px 20px 60px !important; +} + +.afl-sec-orange-row { + display: flex !important; + align-items: center !important; + gap: 55px !important; +} + +.afl-sec-orange-pictos { + display: flex !important; + align-items: center !important; + gap: 20px !important; + flex-shrink: 0 !important; + min-width: 320px !important; + justify-content: center !important; +} + +.afl-sec-picto-wrap { + display: flex !important; + align-items: center !important; + justify-content: center !important; +} + +.afl-sec-orange-text { + flex: 1 !important; + color: #ffffff !important; + font-size: 52px !important; + font-weight: 700 !important; + line-height: 1.25 !important; + letter-spacing: -0.3px !important; +} + +/* Bas de page avec Logo SNCF GARES & CONNEXIONS */ +.afl-sec-orange-footer { + display: flex !important; + justify-content: center !important; + align-items: center !important; + padding-top: 10px !important; +} + +.afl-sec-sncf-badge { + background-color: #ffffff !important; + border-radius: 6px !important; + padding: 6px 18px !important; + display: flex !important; + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.25) !important; +} + +.afl-sec-sncf-bold { + color: #002d62 !important; + font-size: 26px !important; + font-weight: 900 !important; + letter-spacing: 1px !important; + line-height: 1 !important; +} + +.afl-sec-sncf-sub { + color: #d63e18 !important; + font-size: 11px !important; + font-weight: 800 !important; + letter-spacing: 0.8px !important; + margin-top: 2px !important; + line-height: 1 !important; +} + +/* ========================================================================== + CONTENU VARIANT 2 : ANTHRACITE (Attentifs Ensemble / 3117 - Image 3) + ========================================================================== */ +.afl-sec-content--anthracite { + flex: 1 !important; + background-color: #383a3f !important; + display: flex !important; + flex-direction: column !important; + justify-content: center !important; + padding: 40px 60px !important; +} + +.afl-sec-vigipirate-grid { + display: flex !important; + height: 100% !important; + align-items: center !important; + gap: 50px !important; +} + +/* Colonne Gauche */ +.afl-sec-left-col { + width: 38% !important; + display: flex !important; + flex-direction: column !important; + justify-content: space-between !important; + align-items: flex-start !important; + height: 100% !important; + padding: 10px 0 25px 0 !important; +} + +.afl-sec-vigipirate-title { + color: #f7a81b !important; + font-size: 68px !important; + font-weight: 900 !important; + line-height: 1.08 !important; + letter-spacing: 1.2px !important; + text-transform: uppercase !important; + text-shadow: 0 3px 6px rgba(0, 0, 0, 0.4) !important; + margin-top: 10px !important; +} + +.afl-sec-vigipirate-emblem { + margin-top: auto !important; + padding-bottom: 20px !important; + display: flex !important; + align-items: center !important; + justify-content: flex-start !important; + width: 100% !important; +} + +.afl-sec-vigipirate-img { + width: 600px !important; + max-width: 200% !important; + height: auto !important; + max-height: 600px !important; + object-fit: contain !important; + filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.4)) !important; +} + +/* Colonne Droite */ +.afl-sec-right-col { + flex: 1 !important; + display: flex !important; + flex-direction: column !important; + justify-content: center !important; + height: 100% !important; + padding-left: 20px !important; +} + +/* Bulle Alerte 3117 */ +.afl-sec-speech-bubble-wrapper { + display: flex !important; + align-items: flex-start !important; + gap: 15px !important; + margin-bottom: 30px !important; +} + +.afl-sec-speech-bubble { + position: relative !important; + background-color: #ffffff !important; + border: 5px solid #d9241c !important; + border-radius: 28px !important; + padding: 16px 36px !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3) !important; +} + +.afl-sec-bubble-title { + color: #d9241c !important; + font-size: 34px !important; + font-weight: 900 !important; + letter-spacing: 0.5px !important; + margin-bottom: 6px !important; +} + +.afl-sec-bubble-numbers { + display: flex !important; + flex-direction: column !important; + gap: 4px !important; +} + +.afl-sec-bubble-row { + display: flex !important; + align-items: baseline !important; + gap: 14px !important; +} + +.afl-sec-num-lbl { + color: #d9241c !important; + font-size: 38px !important; + font-weight: 700 !important; +} + +.afl-sec-num-val { + color: #d9241c !important; + font-size: 58px !important; + font-weight: 900 !important; + letter-spacing: 0.5px !important; + line-height: 1 !important; +} + +/* Pointe de la bulle vers le bas à gauche */ +.afl-sec-bubble-tail { + position: absolute !important; + bottom: -22px !important; + left: 45px !important; + width: 0 !important; + height: 0 !important; + border-left: 20px solid transparent !important; + border-right: 12px solid transparent !important; + border-top: 22px solid #d9241c !important; +} + +.afl-sec-bubble-tail::after { + content: '' !important; + position: absolute !important; + bottom: 5px !important; + left: -15px !important; + width: 0 !important; + height: 0 !important; + border-left: 15px solid transparent !important; + border-right: 9px solid transparent !important; + border-top: 17px solid #ffffff !important; +} + +.afl-sec-phone-badge { + margin-top: -10px !important; + filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.35)) !important; +} + +/* Textes de consignes Vigipirate / 3117 */ +.afl-sec-vigipirate-text-group { + display: flex !important; + flex-direction: column !important; + gap: 12px !important; +} + +.afl-sec-consigne-intro { + color: #f0f0f0 !important; + font-size: 27px !important; + font-weight: 500 !important; + line-height: 1.3 !important; + margin: 0 !important; +} + +.afl-sec-consigne-main { + color: #ffffff !important; + font-size: 34px !important; + font-weight: 800 !important; + line-height: 1.25 !important; + margin: 4px 0 0 0 !important; +} + +.afl-sec-consigne-hours { + color: #ffffff !important; + font-size: 28px !important; + font-weight: 700 !important; + margin: 0 !important; +} + +.afl-sec-consigne-app-title { + color: #ffffff !important; + font-size: 31px !important; + font-weight: 800 !important; + margin: 6px 0 0 0 !important; +} + +.afl-sec-consigne-app-sub { + color: #e0e0e0 !important; + font-size: 26px !important; + font-weight: 500 !important; + margin: 0 !important; +} + +.afl-sec-footnotes { + margin-top: 18px !important; + color: #b0b0b0 !important; + font-size: 19px !important; + font-weight: 400 !important; + line-height: 1.4 !important; +} diff --git a/src/pages/afficheurs/afl/components/AFLSecurityPage.js b/src/pages/afficheurs/afl/components/AFLSecurityPage.js new file mode 100644 index 0000000..9c702fc --- /dev/null +++ b/src/pages/afficheurs/afl/components/AFLSecurityPage.js @@ -0,0 +1,262 @@ +import React, { useState, useEffect } from 'react'; +import './AFLSecurityPage.css'; +import vigipirateImg from './vigipirate.png'; + +// Horloge haute fidélité pour l'en-tête de sécurité +function SecurityClock() { + 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'); + + return ( +
+ {hh}:{mm} +
+ ); +} + +export default function AFLSecurityPage({ variant = 2 }) { + // variant 1 = Traversée des voies (Image 2 - Fond Orange) + // variant 2 = Attentifs Ensemble / Alerte 3117 (Image 3 - Fond Anthracite) (Par défaut) + + return ( +
+ {/* EN-TÊTE BLANCHE COMMUNE */} +
+
+ {/* Icône Triangle Danger Noir */} + + + + + +
Sécurité & Sûreté
+
+ +
+ +
+
+ + {/* CONTENU VARIANT 1 : Traversée des voies / Trains dans les deux sens (Image 2) */} + {variant === 1 && ( +
+ {/* Ruban rayé sécurité orange / blanc */} +
+ +
+ {/* LIGNE 1 : Trains circulent dans les deux sens */} +
+
+ {/* Pictogramme Danger Triangle Blanc */} +
+ + + + + +
+ + {/* Pictogramme Train et personne traversant */} +
+ + {/* Quai / Ligne de sol */} + + + + + + {/* Silhouette de personne sautant/courant */} + + + + + {/* Face avant du train */} + + + {/* Feux */} + + + {/* Ondes / Rayons de vitesse */} + + +
+
+ +
+ Dans votre gare, des trains circulent dans les deux sens. +
+
+ + {/* LIGNE 2 : Signalisation & regarder de chaque côté */} +
+
+ {/* Pictogramme Interdiction piéton avec feu clignotant */} +
+ + {/* Cercle barré */} + + + {/* Silhouette piéton */} + + + {/* Feu avec rayons */} + + + + +
+ + {/* Pictogramme Piéton vert sur platelage avec signalisation */} +
+ + {/* Platelage / voie de traversée */} + + + + + + {/* Silhouette Piéton Vert */} + + + + {/* Mât de signalisation */} + + + +
+
+ +
+ Avant de traverser, respectez la signalisation et regardez de chaque côté. +
+
+ + {/* Logo SNCF GARES & CONNEXIONS en bas de page */} +
+
+ SNCF + GARES & CONNEXIONS +
+
+
+
+ )} + + {/* CONTENU VARIANT 2 : ATTENTIFS ENSEMBLE / Alerte 3117 (Image 3) */} + {variant === 2 && ( +
+
+ {/* COLONNE GAUCHE : Titre et Logo Vigipirate */} +
+
+ ATTENTIFS
ENSEMBLE +
+ + {/* Logo Officiel Vigipirate fourni */} +
+ Vigipirate Urgence Attentat +
+
+ + {/* COLONNE DROITE : Bulle Alerte 3117 et textes de consigne */} +
+ {/* Bulle d'alerte avec picto téléphone */} +
+
+
ALERTE SNCF
+
+
+ TÉL. + 3117* +
+
+ SMS + 31177** +
+
+ {/* Pointe de la bulle */} +
+
+ + {/* Picto Téléphone Portable Rouge */} +
+ + + {/* Écran blanc avec point d'exclamation rouge */} + + + + {/* Clavier / Touches */} + + + + + + + +
+
+ + {/* Texte des consignes */} +
+

+ Si vous êtes témoin d'une situation qui présente un risque pour votre sécurité ou celle des autres voyageurs, +

+ +

+ APPELEZ LE 3117* ou ENVOYEZ UN SMS AU 31177** +

+ +

24h/24 • 7j/7

+ +

+ Téléchargez l'application ALERTE 3117 +

+

+ gratuite sur App Store et Google Play. +

+ +
+
* Appel enregistré / Numéro gratuit
+
** SMS enregistré / prix SMS de votre opérateur
+
+
+
+
+
+ )} +
+ ); +} diff --git a/src/pages/afficheurs/afl/components/vigipirate.png b/src/pages/afficheurs/afl/components/vigipirate.png new file mode 100644 index 0000000000000000000000000000000000000000..f3320ece82a88acd4bd924008725f5490546dc47 GIT binary patch literal 58372 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

h!ZlgPy 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%0D1`Uv4X4(Bb%*pe+S}a!drF&b3T;y%%jZgA{OM$ zK<tMLK!t&qYd_2%vbhCDtp5%K!makfbBNF7iy0*;9O)i$C*1El>l zO|6m-_q1=RB#6BFXG)vz3XAEbVeTz7z(jIPfJ+8aMc|^6m;U~Kv6Gt!0$mCjCO%}z zE?^_+8|rsY0b(f`ElU2}C@17<%Z;>se&hxAJ%`Wz{g$@TxfFz2Ze^;DZpA-y^Jh!`k#r-l$-WJpM?mZ71Zbio zKWIJ~401#^pl#WBRNG)--@I=nuag`Odp2T_KVSJyy?x90{CR;!egcPnttKM5bPSSk z$WsHs|I7flifNU~YwW5t``!@X%A?qJXHM={(0#T=*HZraWoAH4gF;X?^!pXiW2hIJ zc<_4#DHuk=znqhW!PNV`g&Btc5PVTnD~DUFj{(e8`a}&4zjjrYv8Yqfhys4_+ zP0^qR*pYs%KJ9C|A5s@U(r|-z9|5xlXoCvvazPbRN#7fI24LO1mi{z?PJ#cR#THr& z^t=t9J#ZU_(_bAR}F3zn0dtIWtBt6`C* zZP-V1x%>o>ZUMBQ_3*G9Yle2NvGN0ZJ@yFTI<08^t9kqI<;j(I;sP)-w&$7>J)sHo z?EuU+lktthO1E#20V4PCz?P&F#N6b=DvCN zOn|c z1`RTXp9E|=9+aw1Z$(>hW|ZXjVnw~3M+N2PO%C{Gssg=J^5w7ydUI53hf@VVhf@hk z!rv(gBg-q)>}Pj#fnE*P3rBi&zv5`40Nfq5jSOm}%s^-WbR5SvLPZoaf%(NhBcidO zi7ll{lD1siumTIZNNoYb z0O*rjg}Mlu{w-#=93$>rLhi+cTPXl2ljR zNVAPE(qijd?hfnn8GoyX1*XF^Oq=)b7Xxt>Ua_xhxOFjgWdZL0U{aaKK9}e2 z6-KuNOT$JuRlk8m7-kR17kXDHHOzIA(QWfhtKY5Bcn3W^1&I<=o(gifp)v>sTFbz3 z`KMUt07(}U9OOZF=XPU&SW`6wJ#dL(i^Aqa>5yy+Al__+ zjQvNi@mKrH-7&GW#th-8`%PXC6tyxGlg}YH@o*jn3!R&FQ)PS2^tk=_opq&CH@+jvX7e_{ z?JNe(Z`4xwU5j+`hH=+D2*565-$z1t2*DFoRzS zJA+vf`H)k$55y1_HKeVoyR2oDEL(rp8YDI^uY)QI5QMcpsmvuIIG*Aq2OO+d%R$}{ z^a5d6+LymPN=NiT;LjXDgSyq04fth)-ZLwL z_88VOSSEqbVqk8uu@lqdw@~jmsF-b11IGdX3-k|Gv@>;U)yje&MHGYeiDbxOjBgV; z+BEZlt^xEyUy6CTPv0G%KKJo9lc8@dwA8YD9LOG$=2@1HxH)?g}OE54Wpm-I@(S+SnGFU;GGSi z+>1Y>A>qj*lQ~iR99+2-HuX9so}(Dcxg;*>fV-4DHbe} zPn7XRwoc9C_{VbmP$!B0BZ)6q-s9l!cG&t|>DI|K@?yHPh%H?Z%ZKvhiRd^OACLrfc(N^Vc}oB-aO% zhl+r4`#b=p=L!hqSZA_ z==wF`uiC%9{7e&emp`)of!Kb*3r{8^QLeZKGrWydnBGD3z@-C2`rqH`)UCk+XGQLA zzfcRbAAu>Uc(iV_f-KVfs*D)^lP9CM?%hKSz~ed8Ubt;fDc3^}yMe!uQNd~0tb@6u zTi?zkHEX6_IcrwS5QB*aYp#q|sIJDt2YkSXs73ML6E$HX9tVMq(2P<5M?3u-G*;T% zKd8htjc#uz3T1%pE$<}#dtLStM$9ur#u25mK9a#KG>|X@y>X+(C&iTd-_A{Y>jwd8 z*0?x*p{kG-)s@c>;=57kZ#A?rEy(BUjy`Z&>hRcP07vXl?XY|ILqzCmUIIdCnAJ%9 z_DK-Hw~M5=pWI)w@uEjPA)T(2?aMLpjRx7T2E*HzxNl+sQ8aBm`7byn8BZ>uJjdYW z7h8hxJbFwR-IpI$aQUQF6UDqj@P~f@q35-Fe(FA^0FH3p1pEaDUbthHrMN%G^(|I}8I*kjOnajwNT{h5ORoWAB^*vYnS5R;{m|ZoOUO9lIuRccLgPAy zzXP9oGO%?fEX?|aKx6r6iVq`)u0SO1t#qSiSmn#D5X)aSwY%3SI! zt%%CJ`2lE?tQg{NbAIXre=?*n1q1<+X;5I#6~AakG`}EV$QZf}hwWK8(?+W^Md7CA zp(N$LoE=UQJd>Btbi)KkDIG~CR?BCPPz}JG4lv2HQN0e0Ph4wCL`(#{PjS_{41QmY zjdo!MH1-f+;_QHdhpY|)znJg=69c2NzkLwgA6y&SAL_GrO;d~4?fCRe@ zlYcTbNF?`?Ms-{I4O!E|jx(?$ZA$b2KBN*chv#xCeQO7I60e+_;7}piUBsr4RlFJN$qZ|kHjJxoSa@gQk$?({C)41 zOUE^spM4Gq)&G%n-SJeu@Bj2kC?OF=NLDH%v&^!xg(92mE#t@zMP|qrvI*JqSSQ(g z?@>0#-uri*@2~&zdY&uZ`a~?DEIyc=DTcN|$oSVp|g)`Q_@CLz)8jb7yU|KeG(WD&lCw zET^g@)ZbHwP8Op6qO)xE?%fWdzE3pESoe~N8rNuk0!Zppt6dMekV`W!SX4_m_~2hl zEr;eCrMKa$lW@p)wY{N8-Z^)*^kmwd#46F(a|vv49eSO%Jlw2^gZ98UGw&4d`v*B*txqhs6r!Hp{R`uvycYmv=ODu z7+9=f!fVsKOkq593DNh$Bq^$Fd^z0gzGj)JdaaJ#%Isf!{RC~r_~Yv^$gJe-8Vpwm=4O50-G?PJ7Lanp z$171!Q__A;vUyGDeoH5uXbL%#2IkJoVC?Tx!s1!ZW7YT8`@hQ-$uZn=cgXIx!53G5 zQ?n#Y#x`aS2$@IKBbMs(|2`88n86cgo5gT0M<+2pbmwHHaGy5< zJJej4bXx;%wO@6DUuS_|cRS>$(u?aUCeHiMhtU_p`p5X)tH=WLgU_Q#Sm!dVA$W15 zHi!)8YchYnwy@ZwpHd+7+{6Bp$_E5u!r02Rgp6P&2o)PTbMFq%wm@$x{eTVpbds|T z$UOt{w8wcTxV0?N7?QbA9;mkw6{BgPw!TrQAH>PTz_X2lto5TJBSUpbXDpZIefr-O z`Y{gZ0bdp_AY~w0zOcpW_x3&D_~Ke{^`%rOfO<+aA|B$@c;3{TUR|HCb})H%=>Zdu z<>s!p+@HqvYh!{X32|pos*!}{Xf)hRSdrJpI#$G!rWpI-2!Gi6_t^@RUpGxN3Dk;Q zj}l7Gc9kJGA~<`PGGD;TnqG{g;>}_Qzm3#Hc=8Vy=QUP~A=Kf_P72mHnXcjDwEVg8 zzvHKFzD#qoDz_Km4P&*42m*ngPOY1?Hr&{0ybeAUk7a!)TKoexF24GbYXGaoy>F^( zO?ZCNyp-87U8Vh_C=_xH*i`t)O`BqqPHv)m_V!Y>`Z8M|5wk&;8$oPZoNFsCy;`rg z-pNLpjpZf3Gcd@8r||Kt^1P^(C#JDik^6TYjSD=3Ix>f zv?h4&bF2{gCe6Z(v0hnTgY)cn@|!Q7TtaN#w!a&uECg#ti8{sV%=1_QlMn_bCgoN5 zo-Z~}kL)!e!rZZ$c~|<&@fC*z4x)ax%rFH1S|sMuL|rG?i@i`t5M&wKi;pA55jZR< zx0xQxkULT-l6+}*nIo*(xj~Dh4I<;mMfHU*;gVi2;DghjbE2OEKI(w`y%BUd@ROP{ z6pNo~n$%vgH+*%w5mtMHGspxa)-E*>k*-fBwa3z4yjTua;v9WZbU}Z&5`~!>f_Qx+7avTwe z{6bD{8S^W+GB*z1tDK$e8JRwVXP|~gNHj3P`ZyY~Q_AqIZPt5ZFHs0w@*c!jO$NI@ zFJQ)-`sz`(#_`R^P$*yvLqpG9$-fq)vCw1^K$!3twipHC_)r&q@D~n3k1ERhA@Fs~a&QJ35Y?J@f`XfI_)N-HRr{TeW2!ybShiO_w z37%~oUl@GpvBFkAm&9z%CE+?-4J|0g)!&WdpBl22^lm`yjSxne^WA?s@YJr}ecG@? zX_DXW-=C{hhTeL3-DUa>E|eKD1p>EVIZ^?L9L|Uu1}?Le87jo>24WMOxSKcvhQCHe-fHH>P1}}refB~VS+64ybv#TAa@)9u$xIw! zP!odpSjrk`2?nC>jnLUMFfxudeWhYnPo=Nqh<|YB#=RVMs3H#w_mX>?uB*X!3m;r6 z0|Ub=U2i?VpiIR`V8zjcgnGu?^1fiZnj~tmb7BA<*IEOXL_&PMvV=g^Sk3bD2J9yr zx&@?!B)|Rm5q0lG&n1LI>-F}B+CHKTj|pD#b%4B9hep^ro+psYaPMVC!}KSFABt(unfOE zcU0L;)5UuZcDrVV8~6NjuVNR6k6+KoZZeTTPTTfPAo{M5-6z&q_!7>rc_0`NeY-na zYW1R5M9lufB|_9MF8GR59>}iv&Mi0W0E4aA9AebO2*-Rc{diW`v_JhPW~)|Y ziJke~+YGY*TPNyE!UFm~{&;1MM?uXAx;++`-OtC6)xK3A-d>+7mEB%wZIl=mCvpAw zWa=a2g@}r>HWJobYseKm(q1K!?FM24ajGFDKe#hlA(!2jp$x}Ig?P3zqq+Jre)Tuh zyCFwMD3_S?J!800AxfuIK`k!y3*<7CcYhiimA=9ya(u%?%5Jf}4h_up&%LCc=4tzY&kTOwEQ(%qyGN?= zwb$v9A)v`AgqSR87?UcsRbOJNw#LC+)xM8;v!DzKp}YlWc-%gkcvc+j zgVK8SP3^(Gz>Z9)h!M*fTzc_WaElrHCFmo=rh`MFIis8j(|iCNd%lB^k28qR_I;W4 z1MF&4V0nt}ru(Zrpnzx11=;8~sA^l|`E0V@z{9pdbRNScrg@;BOs6F!|GISif5kAO zz~S^oG}OX#$wk>^RvPV-0!~?K*yFKAj{)Q?f6Np7A1tnN@-TSEb084p@ylN#0+0g1 zujRxgKY{T?@jQ4a59Hzq=-^Z!H*|9Itm0K~-n`p#Wk9_7$Bo$^e0+KcM6OL5C&Sa% zb{ykfua}h>=Z;G)2J-%QiggU9L6ZX%bIcS9q>camXP8VM5YLW4SQlZ#A1*HCWM;Nd z__IZu?)zf6#P3X>p+hJxEdX_n&2e?Wm>>fP2C1UIv8YmgC)4Lover<3n$hCqjc4gz zMb?`D8eVdC*l^o74E0KYqsO$lgOCD&IB;sUymk9AT}K(0&75jFr~Mi}vVKvMjdW;nXCF9ZCQhT!nU9ORE>4MAWb=YOB&cnU(dDVz_ zHmJKe0rCBKorjy@wP=G9lgk%dY|JesFxTahOXKO+{l*|AiaefA31j4X_ zP`?)zij}ZAGm4$_vlIWWdl|nNN6_8ndWj-VT|*U0Vr(I7#zT7q$QBptUvNf%83e(M z!uV`AXZv=*uEpqYL7u7Z=2pFSFzuPDAD}Lv&umpLI84$S44I(ZfbjpSK-D#kGD|in z+0$#`=V{F-(ogSu4+||tD*_- z?sMLklMFspNNjFY+JG|^O;&%t$?K5lu{2KL+B`qq5qCa2j@+4d0n<~#`tfq& z7OgDAUVuMmx@=oiXS{kT{RmnYYiWCvtVP&SP4mt>gAF^vm{p=r7>0K*-S`=KoF-MEQ0JCj9E&vG_?je0i%Re z9Wu9HjyX|UyIQ$JrDpvBuOKJHbKO}e1)D@a$%kwM`D$v=pA%b?E%3?bl>>Y7(%!W& zg2d6$DpLq<|MAJg-A_R7mnOTY&?KgA9y3R)da-q~k>Y+U%jYU`^U1-+bg?_E;;w!2 zOR){ob*CMjHxUT-LgX4H7O`*)^1s;_*qSVcCt*UNLO_a{p@-Vlib}-#JA_p&klr&2 z3x`lKA`m+NTE8`O?vB%8B<+%IriHhcdg)j6y{X+Alg0efv_|`Khho2k1cZVQa)&(j z681|om$J_tdCc#UDut00>7VicFX4ECCYn-6>f2o#kIz(R!?xIT7@2H+AhB;>>KJXU zQwWDRrPLpDSocI>pbxO&7qaxDp5qX%0^Oe5%V@TKN^x~4 zPq5JL#Y++JGr*Quh{Kpae{fh-su=b{{0n+++z~Qo$U(wu@__|<415v08{0q8Ge|Xk zrG3P%TRVdbCZ@z=b)HE$S8!h)#5_srq$-ZG-D>^x#oPl>S{ zE%J{CpHq*2LwWIiSxRE&bfnLUOd@re&+fzHrbWS^s4P){j3+}Y?SI&Mf&Mh`Fc1il zlw$MYJWYDg^=HsUc#ryS_SOEhGp=oN1*)r-Pqb_KAEfTiS4^0M0WSB!ZDM@nh(`}N@*LW z*#7k{@|fh8mXS#*V;Z3=UMf18dEsNaxGMUsDH!Tc=7|X?EmXF)x`T@ij|-y59vn@| z1Dz!dnoG*Mq?l0kGOO{2&O3AeQsz=f7eKyRkqvWku`BUxc^Mw0DU#AVmF}wIGvc(h# zsXSu?c62g2wbhl4cRVU%FCh>O08XY^L@P86iJ7O*HH!t)2+2VHm|Y5IJOESBBGC4K z4h+4j-KbS4=o^0<-V*J2**U#Ydqj#3C&{flY*>cfDIf%Q=$<_3W(`4n#sEH_D0rSesm9O7^6>@cFd0!bfTQjoM6oKD$(gYUVy70%$^M!VV5tbH zq0a=1SnS%e)uE7)i^(|ll`E`Nl1>{Fg?mfA^4>letq zWB!^h5onw{R>@(wc@L%wq2p6Dk*a_zAjxK=3;8j^X51aAOB$+XBV$(;AIep?a;W^}#C}D>b)MdS56svzWwu+jXuIP@In?J$?^V=4NAJqs#zbJ2fx>ZeZheG{}vrUmZJdbQO>4vfh_qCd|M1pTe`0NF?{sg|DOcd zHu_9_g1jpofEPA!n&hl{q3pysEXDYmN#Xmw_wV0hPjhR9S!y}992V$z^uec46!X*1o)70MuA8u^o zA^5>omrb|o0XVHjYNL=X)1Efa${B7SE6p(iVquZTnZ}W8yD>1!%0bbp1$^s=@85UN zL7=GiMa0wfGl({eCQ8tP7lAWq~yW-?c-2rSaaTzl9rcgQ3;jFKF z=)kJKiO~8ek--EOwIb8r{}!F>{kd%O!>Gs0pZOe;q!O&>sQFtvyE{_U!x}cReeg6e ziT(VPxoaa~a3eqV%Fg1QjkE0-qaw7k{A8s)3OWgL)|1ux)x(Lk^#9J!T&*i%=Di$umLN^|ee| zPYzX=z7qlSWFGs4>#fjIoUbzgE$sZ^R_evh4wyqens!r}+)mnSAfBdDe8KgOCd_xR zF_641l+Gs&#HiS%ur-!mvr4t0K~yYgQ*H9d*=)Kt4+818!lKMaAMYr2m=|V^>*l&C zI^^_1lS!ese-*~KYTosoq{s$<>(T!j11K+5XY6o|rZ{sg59avEeSfNh&7p_$bx(3{ z%RkZ%3x1DF?R3h@@{BZh8g*^H-MGafo==etV#e25=7{9i3s-)L>T7(X$b7>-==~pj)(W z*Gxs|*oj8a^rN6B&0<-1+?4BuZ7o}+U0?Eq9M(V)nl}Sm@R6C17NtuPc(ReT_TLvz z%0L-Nz{VpwH_2}69c`RCmOivw>{@fU2!YO0sI@lg?fHpWi`j5lY4sP+^HCKI*8SXb zXUd{oX_Npec5M>%QZneaFDOlD|#nx{%xj4+^?ZPE|XvmDMRWyP7^R zn4i9J>yGtzb@|q7(uBm$c6kDnDXI5^m}HpI>5_WIH*Gk+xURe}X`?Xi?lo>)DbgOophLh{H6IsQq%c0rgJQ$N@m^pq}74eiE z5%KKVCEv*)iO4`-T! zS9!JH%>3539vcv0b>+NZne0l=d9q$HLoMXOFgYF#~u_c zT0I}6k{22Kt9~jL$R%FOG|cq$kWU$!{*K22}yVYHEg+H zRZ`LPDloSDR;}&d|MmpYM4;|+DwF=^@t)kbSL>eKQd!gpYTY7cyUCfWKTl`n-{Gzo{~MQf4#5laL%EFt*_`5=n12{ zx_uJ*Vk=!ziJM^N9jts;@`7RVl1 zdkm*sG9aCj57@yB_;D$H`>@>nwa{--(Da=9o(hM_`%&NxL4{81Ur0e$Y{^ zJ6d^|6muH4Suh@0!%HJUAWvVt1 zq!l~tD+IvL{Sgw(^t)GLIZ>bO01SELE8Dx$<5`g%l8=?=@)HK$e&pjqJJXY2KYNscnYi^cpk^ys!pWD?AqI|4iPT*rGFcCkKze6Lq{jRC&oby^zS%T zQ6`1kJK>;8KyTCb`Ik0%wn%gHBU{YP&|c2F88-}~Pm ze52q^^g}so1rhk4sNRr^EEJn4UPa!y`_oZ)BD(C)O-x_;!@Nn?N%z`cV4Ha7 zynJtCSPf`PvIX=bcZTwHEd)7I6l4>18nCpq1jT|K_Nv!H;ANfKGb|KV~xCI{}%Q1IFLOo@I^%7+TL$ zV@qbn5u=Gc@$PpkLk4O&7dWV05MyYT6MntmSX88yG1{F@4=#0B#6gGX9sK3*1rvR9 z9twkqUHyH4W3vygBG3D;s1PiI<)bZ$ZEvs~!DFYApo_jMuE2X)XTd0sw!9#Cl`x+9 zqJCvM9ollbf(+}@Xa%(u?xmBq{b;wN?UtZB{mPROUV72B#xJxJBxbZJCLOOLmpy~a zEk`kBt2+m&FO9~gc}%);$^2Bk6KwDV9D&Hg@|HtnFYYS!%c6IUc2$tno;E-hU@7v9 zfCV}d<&TG*4cDiJ49GKfz4Oq0D|Hr!o5|Tku}|8gs^-nc!sz{~nMRh$v!Gdh{5sx@ zFbfu_P@WvMbn_q2&}?VLQQQ@=jw6gC>AGP0I}Zg>lx}=!aRE_UcW#|5?X?M+zaNp_ z2^)AQ;zglYAFY@+p8m#!~`fyV@Q&p6$_C)Xpmv_Vs5Vef-z z!AbLjP?-};JWlHU)Bdj_E&XJP<@E42^i8~KVMjRc1S+$__&yR+mccsRB)Mn02U|Wd zKuS$p%H>9PiFhaIu$Nhmis$1{2&ll8MhvdO7PNaJ=q|ivo!NBRD5(#(CnMxEH$Aln zO~b>~lh=K6)Yz$5tMkrJJTBbr0zCa-iPq3*G41`Z*u|QUkK{kdlU)qC!(aT63u1%h zK4waypuOJbEcK;(wB-}2f?~<8yQ4vp%&OX7X@%ToqE?m6c`U~MIG)qbBsgf^WJ=wl_o;7aQ66QAdx9Ez6>tD%IjB8PHXC-A*EGhbYwx;bdCQ$9}uX;c065!#NB9Tk2m7n7Y zbV!BX(Z&uUBw%}y`)gx|69*C(G?O|4@BVN+`>^@3*HBboEq!hxBp+AcOzUTmTYlgz zR!g$!^P8C6(#{sEpfN;_S~pFU_E!{g854DsVwT(xO?p%3Cg$<6T!tE?j+`~tJx#(Y z8K=_pw+e{jr!L#N z)$y~r0}R-)sdUHw`X^x6 zg;5`?2$p+Zu;bAc7@emh#Lrym64wtqC49Z+NE?^+*p}Q=l~<|vekqFUu}5~nh1GU` z6om$~tHM5!5lMG<=w*I(2XLHo&OVbb9*bWPi@4d4)w%pqq&ff;cHp*aq2=gJgZWnt zuVne8RSIx#4)-{a!JS*?7X;BUCsJ?Q-=spLegpg$zsm@p0y z+{GyS+;kaozS@_|XC2*1e9f#Io-TKv7Ie?^2r{DA8v-JQ4Q+=5_A)Qdqdg=gaRn0I z8}lOnP`Te$OnWJlwHlr&L>59T&;+G!!T~Y;aIvVy^X*37B-Y5jqvvI%8G?n6;rGnbKEe|ylKPUx z1X?Mn%{1o!EUc1UD~Ey~K!6j+0u_Gt5-5PJ zN7gkM8ppe|gWY(0k*z z`E^yI$7Z(5@qAVe?R!YrVlpY%r04n1M}x$)!W^PCI(R_qvd$Gb zX_j(yd7E8|iyMyCi>}hk%tsA!flR`beK_Gtc^#fWY3rTGxl^JUkKVw(6jExJbMEwK zsuvNes4d|U(->;u9%DcFC(4RMu_^)K=JUcbmG{)rohY_@A!y_WZRbddm!sHmMa#%M!s=T2) zW+7xg=hSK~yTyGhaK61GZcy4>l{}Ch45FCx$2CgoM;y-s@3W4nP(A0QA2ACn=~w?) zWufy1w(Kxk9jdE2HY=!uRe;pUanp48SFl!%OXG%5!g=R$Wzn%tIz6K>VOREhy$j*? zD&)#aa^g23ySdO_v%#Mm!PZ6$Bk939i#zk3R=IyNKO?m#>$PySwmnk$u9Eyw#llA`dXIs9zwS;N=;ms*co5DXHl5 zy9Cr@|+lqf0bDE4raFDZu8nmirJ zxjzv1qD~DgrMF{_0uv8ybx$Zl@8BbK{tjfT^a1!B^;iBANCTh(6Fj$^%(Ox~-uU^3 z5O;Tad9?V%#K-#Ob=n0t_uNN%0*))889RotB$%L~Rnn%)c`mgAy>l|5$w4B=SfIz2 zW|%0RvW}RI=++80EzOuX&d_q()|(K1hCePl|A^(f;8az}WgDygrOMc8lC|r;+?yKD zc<`!b$lNQBxV(gFs~~w=W2Ckwt{v|QWCMgq8BY@HY#0j;6)$pENHld{Zc_?^gdaey`Q7nY4iAzp5y9L1dC@z_GU|v zUgr3#ld9zR0pxy!$|Gsbq_wo3=SQez*9CU8g2~l&N8Dt9VV{gGXpTni`#_P#NyBug zSi0=lY9McyR@KvyvF9hA1ONHl zQdk3yfVXy$&a;hYE>S(y_V?XCCrZf4$uWrBzz7bnYojKFecI#ayOY!)8N@_R7L?p_ z2woZD+3y*Ze5A&9abY4ugu$XfZ~3pkzUVAD&4X9&@D$wWokDXN^84Zzxfq)SF=)0l%RF;KwxDK)m}YLt5Bc`emnH<}J>@G-y3+8qOYJs==8>c98n`&!=wriN$U@EHYlIh2@}e`SC8y5QrJ_)usp z?%;#EhIj4pLdfOw+UL5_s(BhMfY?^^54ivM?DwYgy@Qc_w;N`nP`fRP#i+F|yDmQQ zL`u;)orx6ZJDL`^C1ff4H29o9J(@=W{Vnj zSI}%bYOA}(mh(d-)2=snlB~R@2Cnqqnrut$#LsmZ%jx8FSWkX~ckFoM0K8-R_V|p) zROJG3Z+Zge!BSI>gQ6BFBf(4@hGvVMATx4Cp{BQTrg^rF;^u4WJ87tOcMU%I9GO#0 z!S?;a4Yl|yAr{Wx z=a%~`Yeu8xa)xitcK7`dm@(1B-$*(kKWFxAAdwrLx`T5b~-U-5a!$Zxg z7jjmKKT^ryV@MEmp4g?shYCB#QR8F$6%%jmvEScx9zh=?^w(+#?hWcgt36%}pN$Ul zj_rC?iD_>~QBrDf!LIPZvX(Lo>3Y9ay~`Q2?J|YA)%Ax{*?{55{=a)Gvp#E%F=~kB zJYqMiEHX0ONSqYDwGtt$KBO)`^!?H@J4oD04Fi3Jd4>n;cF)~msR79VE2}1V`KgI< zJ0&BuJN5$fGSOOUY@Hq=aAV6$pK9G*Jb5F&12*Bu1i1}&*|*Gct~t-91n$z^18`H1 zHS-es(DVFi*%IZsesxFdH)ee3cSi#kgA#?CMbc>^cD~zByp0*<;>1O>%`W39;>qlQ zmkURR1J!rFUj1R%F}7&Vab+z1+d#hVaQHp#3^^8qoGdDP9n*)}o#n0kh|Scesm6vA-2a#wMO%Dr#bFJuU+j8*Qe;v-0Vu_^a<+ zyZE~9zbYR-(#Sf|+sDL4RjX3o$g9X+z4i|r6Z!@dSh;+(Pj{XkIrgzzt6y0iOSccu ziS9U`0#054w2!{(nY_P&?Y^AW80}DWd%~R3?ccN9+SH$J152YIr4Au!; zbG5Q|SjXJ?N{pm@4zRdrL2;-@2tZ5`DG8UMWXX3~*@zj!Wij5pzRySQSyob7owJtA zk6&Y*pb>T6fi+^9BC7_H!37cWICKkY_unDx!#i}*&3~TekLS1E#X$w#k({bjdD$Lk za+m^;O~#4uKf>LPkj&}@Y>hffy4!(|lvEQmQ8IM2J>t-`_4h}ELofHZ}4 z`3kN){t<5R<3)k&+2xyOG~kIU4H6?0Gpa-IS-c|_kjHj|OV&1>|7^3(PiM4By?wp0#=x-0yKuj#lLXD0DDERQl*G zdVmuOCsFXuJeRK5c4VxhZpsz^%<6SjZuwTIYD;)3iPW5$|IZI zr0OSK)8()+G-!$=R$y;+w0(QlE6_uvzir$JM;qlL0sFekPcDIYPWVCZ`i>#7cfvSe z%YRf;kB4)IDu3|(r#t0+B(nJXUlMF2I4F2uP6R6JRiDXJKVri_oBGx$X@`xeH~;=6 zR!!WW4IsbIFa9P_vfbqHDZ#Plh0xlzJKczT^%?`_qimMwv`A)El00*OZTY+_@tHv?aH6_c0Wmb` zR~HK6<4~)5EuDn%UjF&uXf=O1mS(rKrD%JjEmtq|uUR8JrdyDJ4)dhUe*ag4IS6Oj z9)~|3nb|LNl$Cs&YTj*KOAshOWg9V*QXnK;qC>xEG7k*l=ut?4ou2J#`HJVDrM~Qv zH5Z6x(;PC*Rx1x-T=WtVdI>w7^`Xu*Up6213B3~A7=WyAMOdm}N4Gnv`Obk2FopZA zN1-P?cf&hjFN9Wv4J)$>TzEHV3dO94lm5`)C8zHMvhw=h`<_`P(qM#7F~GI2THEWx zHO1IVbDi0du~NTEm5ZYk#@Fow++^b?>ttG$Q<=%T4+6 z%E;9yLH94^7QhUny@AP1S0r!i74HnIC7|MHToRv5wXYJ@tYb++D6|%@Uk;rMS%4rcA-Mz#kkq7!Z>6}Q<(6o+ zz!~{14M(-n%u*BK>SUSwrmUAn`44sXj3-<^lb*(MR^I?7GL)jn7ovY=Ia1zKU_@R1 z!?^QQwwCJ2#GUh%iW-Yp-wxowax|G>(}VSENMf1^zA2F$wYA+UK8v9a?ko6K;tM$k zf2Nr(;{MhD{fhntc1eBJ>ZE49)n-sh4xnR{me(9Vb)62f_IF6~@>z|&Rs{m!PzCH* zlm{t++h486-&Xa^9+itb$Aj&Fbix=9*JqLxX zqUE;w-68g}<)IZW&LA16G8&eS`Bzbq9SvUm17i zZfULaY}HO}T!Ts7IOWqaoAJ05dqXHs68c=7fg<)++&0-Fx+^>;MoO4&Bt2EXtgjofa($5U#d7|bKRm?a7tWr!D{H-RmAd&b8hFtBf zq|GX)wb;Q~zKWz37F#z@top=Ki;ZZhL zX14Y%@5f%pFQl)mye5!P&}=nmHn#Ox@c(KRk{7!+XHTA?>+vet;2kBOb9Wks#QUUIvR=-VQv9mzxueV#Q9Jbebe(uV}8YuEm z70baOYrC^`yqcR9%|7eSKe8;d1Z=1@P{<3HGIE)OV8G0dRK=Yw68kaR6sQ1U9yxtE zv#PXignMovn^ZYV&m*bsrMZ1Z#033S7%N2qE0jfhiOpSo{AjSM$70;k{Dp;1*f!es z5p%%uD3Z&#!!+N9%d#}}C6R1i9M1%7hJ4vi43D^CO)E7M2Su#?^5Wbzu-meAfXy3t zcih%f+3vd7Jm!&iLgM^UhNNS;qjLg>M{axF&akd$YXP2KY@TObt?qY4X~6&8&6`8k znwnJv(-^KKZ`b6)8q^ zcO(fjowZm9^14MnV)@b+9no=Z*jdx+u6Vq^EMK$yPv<8W_y53ld~01mK850~x#rpp z%7P!q7ZQfvi>?Ohg4AEp_3__1gm?}^aRTM_E*b?VdS_LoIwxc*4d;*@28G5A)g{4D z6yG0lA6cLO)ucz;g=Q!p{ID@K>A^>eYz(-Koic;4V~3?6L-M*@;*dHhU_R-+v_1J} zK6$uhaMTym!&?n$FqiM-&41xwzk17Tu&8oeb>WL;`s_3|z!D^4oBXDo+zw(B(1Q|$T!*4~_`}(Qq`iKwo$}@C zzWjJ1s+QwV+Ju_F?yoabs*-~&+ZhI2aih@SCAO&=6h@p_t1XTfYBN@W4nM%gnBs>c z?n`sy1^Vs>jvkIWVr4ed`_t(8D#k+Ktwf3J_W23&Q!>^VI04mUv267@@Q?og9qFPT zVy!`OdBR}p@L0}yMQ3;!Xrd(7qwRG0ei2>r9oCO7Hw1asM$aOx`F_4Ftfq>~cD9k!107-#&3So|F@|3r9Dd2(ftU&tv-;vt&VNd7h0tywOg^0*W8m)KYc zqIVAT4z^aS%?GbRpoo1>PFGxRvf>&+@2U#STCc|%70(le-QO*rh{6Y>eg`vvCA9%J z!1_dCt|d|4Bk?JEWaSWAfl`LzG4mo~nhhJ%<-_6ktDs3{?Mw}R`8GIa*qbbcRmMv2 zb*5~_QNSTUE3LCHzv3egFMjFPSwGVKbzS!+h3;M09jXiTlZ?7k6HzsIRT75>)6BBU zeHPx6%$i(#p+~#RD#68%j}5s}X?vEs`;*4gr0I{=%5~>|f8Mb+vlheNBQ?h1B&BQG zVUYwUK`gt`35yzSZ>(O^smc??tV;9rvcb#6}G6$yxBGV4BZTlD`GqMP8e! zx!kYea9@fI5gU@%7tOv9V4YlmWJWTgT+DZV%!8$ew~I>hxIe7%)b%}GpCZEu)A-w- zpFYFx-LShdsx1@wIPsDuTk&n`bBFeMj5-%sFvE6TKjr@a?jk&_<9V$9m2LP>Y05@F zWVy1vXy5MC6M)#!4?NIWJedoL^sFJiPRkpCT}nx*!?-*q-qyul#6)dGcO}-kuacvQ z-i=op_`O%~vx-@`9C?5lm47SSpi%}4vP{Jl6J7_|%&uSdPW@yyL`d4Y^(sfBll?r- z`EHRk;OqcFi=}-***r|6`mK`Lct+&u`H4-DUMxMT#4b29ka5LxP~&(3g%UI*`6c8#*>8;JD=mdELm+ga&k7Und(GF z=(~>sDb)&4*RtKm5&3uc9lrEZ#ur4nfTs9~YN$iJo=ZopkSx?A-}URtk@=cs4*nf6 z!kx;;+=6s}C4A4Qt6Y$PFFXTDYOZ#%hDZEq4v5;tRtB<-a!bs{9n~5$bSSRPbw7eW zIair$@Yyn+fx=&)#{{h+1-@&+{c4PCmp}!QYLM4 z3zi$EX*-#K|6=kH zrpci?{{HM-h7wD&Z`cW0RZrJx`|)s8h13<>T}fy!CQJo>C1Wt7o(U4|N(MX{sJ*=R zxOFix=|yhNL6#F0Ixw}_{D^2mfkE2_WRu_xG*CRS%;ZI$;FxcysA4p2@hQC-kRoAUF27WMj6w6E6cTu*xVedhkd#bRqs6}Av zR891-a+-&$Za8+&y+B!Vii&Hz#4;ZZTgyGcan$=b`u)f46aXDLGJ4nQI@@G1hbgj2&fh{)$ z+zty0J419isXR@`=rO#;v_v|Ew1LJ3QuTSdFXR;-ef#!j<9K2CMs;Ll5roS?4k^ zRL_CA4`N2bVPSKs2F0kj{(YbnQZYBi`L-x>?K!%UqmR(-D;8h-*PsKbH@JL1q!o@B^8*?z_WRS+bQ4oOie)rc+Hc`B1PM>}){GvP_z}>$DbI0ARjS75Vuphpnj|5XgDfE9w*-r8i>}ry*Wzq(zCg&OKd0+#`n{n~h zC51$yVzlt2zxdeA&yE4GSGJl=zu^2PEUgW56wkCeX)At=f`2Z@U^F{b0(6#;e`lijlz=MhZbVZF+xqnRIz554LL4 zS{g0(Tlxh|Aw2BLKk##p&`(@mTtcD+kT$IH8L>&vUtD3UAHos~Xzw>rZrz7lic{xv z*?o{GScj;aX{DCj`0W&C9t|AkktH3tF^FC%po)vFN zMoTR+3y0$f;M4i&>Aa80j(Di@P2RO6~9907U&xfl+GBvD&^ z?477UuRVl<2QAo;7T{{q#S5xa*)_5Z>^Jg_wGKLjb706d}1iw zo>|z9>HhDAxp6ulMz-}k6rh1coDRqH58nsI6~T#Y1Y@Wa-+fMGZQ_NEDKoui)E9=bJv;s2*9 zfIbAA_3@(6^*{l;Qx1b`ndyoNMROc59+_>XLBd(tzNAF&T($XGED3U_NMNGCH-~8L zo4H9&DSC!U$yo2JOob*lAjL#u91@y`JJ``e%qU}mm)})t^q{4w1keL^d`LqP((Ty z=_G(i@6sb(S_A^2hfu$r-}|j~*Spre`N!Zs`(&3nvuB=pW{&Uv_2z!{^kh-do%5s~ zpJ;(_TmL%Py1~c@+=pKvLBcVKKiL*!GlSD4q#F=iKwg?YNS6e(<5Uxfi{szTIdX5^nz zOXwBQ_T+U>mY@lEb>rPjD0&}|mEg3!Xvqy!vFsC4SLqNTSvw2h>VpEAAm#Qe$awI3 zOKR{+>VH)I-x?obNJLuYNcwq2(w_blSR6=EUG7J4fj6xJfBi-2a0s8@ ze?#&D32DsTAaZsUwhD;O?dO$bL1Gs;m()D;KORd1lB3twuci;v!1Qqf{`p*sVlGlC2PjAsK^4qN%s1Gy-0;jK!W&P0@Uh61Vi#S4MAee|KDix{@B}QugKub ziQ+Loh8yIPAMS40d^MFNt@t^#dE|26;>2k2DFY6?Hb;QCz5JInvvWUrNpGqqFF`r)kX$ovQaQqHdWi|lPN({Zc74l!3u|bi( z+nO0llfv^*)#*i(1@K9lStvobpV$*n#Lacw7_`A^oTq<~&PGy>CU~+H09kSo)PQRQ z0al7-Q1JXNyo?py{9jv;kp14qXp3O9{-ZpKblrb#>eVGWq?bi*9lj*s zR`e!YuP$(p(Dt%`jJ9(>%m3jvNcb3dc+ldQ2LGe6YH-3@J>_uWCv^@qugW-Hy9+W7 zxBFibKth#i?8Y>yVhHH!vhsTy#Ls)caoD;Rg!ncHkPmea{ktwaq@GPT*_T10<7IU- zvrIS{3b4s0K`KXQ#($fl)fUN+1UL(kAbNatp@WH>%=;o~)#v1*^+Cd^FepL;bSZqA z7^wo3_40qhp$ZD{boZtOc7e?5EH+#v-D{9?;XO?Ht=mi_3}~SvCcNV^giWQ!Q{?~&0B4kTaJ8t^o}sxPVck`7%^ z{Oj$}e(X%ehFsp<{*+`Ok-oAbu1`#J?|0%-$mgLsgLL$U@y3fvR$ScOn? z@G#xPm6K@4?JsOVP2JH_2qZ$B!wX96C`rloD4hp6Zwuyq7gTf9` zpS{sPNuA~bCce8pISaVqODGd<-XIlgV@vLcB#HJpru>)h{eW@B1=e|m0wxuOhU2r) z1*E1=&7hQ7PjVj#m;g;KG{D-Oz$ses=VIdz z$uoJ*oKb z@7!4xeCAA-q3dkpY^mbVoJ{#De^?Oby@H(E@=xS1^}f3T%7~rUk&kjvP}WzHS9;L+ex-oRX!kjmy>TQ&DMdquT zt*#rKnN#W$2e$l5^74^t`iPUr`ZbnKY4p9#zerbPvBs{N5DZR;rMv(7i5n87O7CBj z?PEH*DgDluM`it?Q=i^9gfHVJQItWmUVv zH^TSmo!p*OTU$r8e^k<0;80@d(AHfl6wzPjc)CX$x0=0kHk)jDTdJp98SaGm=im~$ zDJk#nQ-1}84e0jrg?NsR%%@?vB~s?chJS<01eAPrkZ*c=7)yShXwIWso8{5 z(Pfcu@-QdFE6rgb_l_E5TDQ(;UHv^-;7tDqGMkSf(j|S`{^)67U?&rK{)~!G0Kt&7Y6}KjFwh}vZ|NI4j&HY&AIJzy zsYXi=(l80C2NI(n zz_9t0@-Mr6rUm~dISPdCCYlV1jJmsHg(%O=rSP9r@39;=H0a_Eh~Hd%5~S4C{TD~R zn0gqD-(sC)_%c51p1{h1R%-^->IdCt((=X-n9h8+EuYT@=e3Oeb7axkPXo?ec7Iy6 zygxp>*fFDZNAH{7RD!nNJ_@hbHYitkN6N7BHMWybW!PtA=(z#gAHR6CK_f8B}fU{fnUMktNO&v%QaeCz=K^i$j{p z-@aPY@{G#{gkEbp4w0aG+xAVMJlEpPe{)Jl&)3@teN2(eCab`=CU)x^?jMJBKHJtW zmqwA}Kf_{;TB3}{tzWgPg0m>CAMYqOi=u0MeiNG83GeG{tQPAB>HUt(>1({0Y@Da% zmsDeEv;B&Poq25%xkvi(O(yV|V|kfAK`sT{>aSnQm=UIwrlW1S7~@lm@ynNWCPMnG z33&gfV~q-~+GWqz;*8GNZ_J?oCitn|k3ZQU-#cC@2~N z%&M(lhi6jDKHl{^fR|Cn)*jnRnuedH)imW7u36?r$enupk?P5aLN7y>8$ae7(%RO& zI3aA_5;&N5C_j@;4H{^=+kd@Y9rtL)&s5;&)8vFg1nl_@BN^L+iXEyq_4J|V$(;<7 z!)ni7Y1k}H%sJAN&G19S_lGtQM5!`e(h}J-qFA&50mYagJV0yb;n0DQ|hO=9Tun4xmcm^$0KF` zCh=Wo?WRs=4YBmU@(1eCD^u}le7|g^t?>g8Cmzw(c1i)5y$pwpmJ`ff`tK-<(4AMD z+s&$!GTAv-6AE@yEQQNeZ9)TbbG8!Dr!q?zPKP6ukg2VA#XAwxQ|}nKZr4C)=W6j4 zG#~R>>$U7u$b9mBu6^?SRw27+LIfK=C*Q@V#~liPW&%A^GiWW5T>V-;KajvapoSe$ zYNXIdpQ)<9AhMFhWJq|KbR(tCb>^`*R&4Ql%YHb=N)E@9-y}0^{ z{?@@9*+D$x&3t4A#ZI29=+_rN&iHfWIeR^0;Cm#kavQd<=8MPa z<~N-ek8N5;MVlVd8`mMpkRLF0dN@|Mlpn7f)Ft=Y_pU&(Cdjq2QpQ&V*|+NziO4rg z;d;r2ogXpsxGABBenCd39odUQbqgAkdE|ix1h~G`AJ1Gz;sx@UwA+^RORWUO67{}a z8>Bm0`~K?Q;6sa=)Co15*D#k0o|j(K*CKH4gaajn7T5WjDdz^m=f%X|2t$ZB;KR55 zEx%j;gh%Sd9|VYjCBv3Nv9%iIkJNP0+8eiFWeli!jsRAP{o{dq>XWrAKhRG|E659@ zQrO5;PwLQ)FUC)AO-JvF*X(8g{-gh|?2xLQ<=JQWvs`pTc05DUd4F>pYoM&*wTDIq zyWhG?U^I{bi#{eM&K064y;skAwPYK+R2!;k+Y$GRTYn4dy1Ir7!u)BN|DM@%t?@wJ z-{k|De)vG+%RjFbm1?% zL7fV$iRs^==#1ND58r-Y^`wEY7)&iZu_AWg5vNkf*UV==dlCe;1Z|9CC153wLMCgx zBB$hWX1uVxZQ}JhJN~n#>vfk46ntnPV0j}E*H%?$SK}!<%%bjEZ_CWMUU=AgH{ts> zQ~u?nr?ilc#ZEXwMtnkp!iF>-?AVWAJ9Gcd)@==*leV6)NtGu!M?VwZS<$lNcKD|d z)21OY#~+Jjz`TKC)sXHbsyeYkIbzPib7XGhNcU*Wo+rGAaMJuYyU;J7=v5sV{u=2@ zSLjVa?qcQ}J0b#y&T>rYUJb9l)SdD~SnhiS(&^J(3{VP3Sm`7u6vU$%BWY0b5ib@4 zkD1JC11iU3shz`r8|z#}Ry_3klD>=62+2}x?UmH$V803k*t-cMIkRHyj#eOv2`y?n zH;_3ivi6-tc7m}9Y5Fo3e2H~E0Wz8%gLUEM`E|xgZ>Z5rVkuW{60#}c>7*=osT&uZ zuhs3a8x+RkTZ1*zi>_5%iq80Y8j#FTtfg>Z>slo!;)Z2_^76`Tc)m%(JbXo%(T?7_ zTD^*RLylZ{82{tcAf*aF2ho?e;?>>!*hMexdM#M9kudR)^WwQve~IZ-mITNSoTtPq zr2fp=Vzm#NQsu8Yaeb z*Mt*xk`Y}|z%4R4wnp1pW{IoiuS_ca0A-YU<%OVVw2I&^rS?<9&1ahpavUk(N@!8v zflXONnK<;>7qUdgY#RaZ2zt)Qtg%!Pi2Dcj{4J+}!44NheIqw6- zsZ_XZ`#-lD4CrlyW*2moZ0}0W%UTLD83$R`Qt@d&Q|tNy%*F25Q~6*^ydIzOVC?;z z=fupCD-R^1`F7UVM-0pt+>i^mGv$1Dr-GgDIIp;kE;p+q^_oKIuL(uFjM{y#iDYBE z(H-DYV}iaf0CgMo+KW+@eVh_+>65lCccvR1S5$#DFijZ4IW{lUHxxXLiMuPQ5>z-J zy8yKJxh>o9gh=WE4Yba$lU@%w#i3Qkf|`bDUF1&FtLw@#(gRNTz~1AILR<~kVuWmR zRtQ#g1D}`F{Lm$?USO?vwkNm_$|(D)i$EuP(p=13#PVnEO7{LcQG96d&Vzw9w0vR3 z-*yAH(gW16rSAt|V}3M7QZ%{TRF+%)wsmi|sgknXoQkKE26a073Y=%HU3&%N*w(V; zV>0y@)kE>vfwlAH0#ZrwE#EY27V$CW-}S55zpthlB4mr_tr3K~_=SN?A^y&Bx*ydW z-yJnCJxeVyl3Pfgynloudhb3K16y|k^`k{KoZ?d@V@g={u^&<7hq9hqL;(dQBg6KR z@aZXe_f$RNMs|Qo*R1GC_+yO|#sCmi^Vga>&SZQk5M&^0Fc&$HW?HR7#0NfifC0{q z%&2&IrT)6%PTq@C@1jTfD+6tOY!!uu6}9h}kR*%6vpiPs!jyt4_EXabV{mYZ6WNPo z;qRxW#Rr|C9gt9N@_=MHER_aKwC=a^uWf_GsvZ9#Y3~rJ2#sg!Dwgr`TJ9$G^=4UH z?UTSztDAWK$fTE>7+j?|+x6%U3~kqj`sVC=HVJWTNWk~nvAMv=W??BaJjrGh+j1uy zTWDvg>Dv5`ANc%3bhbLJ=poVjg*~cII>yIPb7L59^uG->t5^=Is9&702vDc)VIv?HoU z@%D!y((mPKrMw@z3aw)bhj!#G3uq&_0j*ZT9Lt|*_UWF@^yNKiR8g^@pl*x%vrxY# zftc(smC@zJY#VpTv;(q6QpDp&{c#7@xw${Ou}VGD9^>N_$)0)y{$$xaW|>K@IZE!_ z^W|KK?(QtVvAzAta?fXXaE->v1}iB3n-A~6L{H_dQ~Icfp#%bJH+EJ{l$@9U){|;S<@5QmGY*%s`s>n#bF`hF%1MBKHYSo zVtJ81Zhv*{gfD%CHGp#k)>A84RG~O*#;hM(x-F*+&0$@P8ci5ZTg&U8{=mx4$;hW^F{=#j)MKFZ_hVh6RPq(Tm^YXE|#XZso^Oif$6m z7P>z3$?BvEo~J?{VLd|eA<4&HpL{Y*RiZFeUv0u#M}>D8cU-?HunK2pCi{0GlBQp5 z+S^}ZG@m=lJJaG?>dP&G!xV6iSIA3@C*hK!uf5kD`uaYAliyDrm~9;^4Su7!HKmkL ztQE9lK-XyovK2$WF872qXs1@Kq}u)-Mjh&b(XyhHde?-{*nxQ+w@o8Y@d4b=zmb1| zH>IPkS23wUTT$S;^ku}zf@RY~1b2U;HswpdAlxrI$FjxaR;pfLVYvdT6XGc44F5&XSw7O zjuRZ9B1G6H57!>pqd4;Zw8z=o!OOAN3fAw1B^v{eB#Mv7LqT{*2Y~OS6@6NQe31I z$9ndPAoIYF+dK?$q;0ge`caSJT{(Y-KWa5_xhsbq4AngibJD=>YRa8Vr#(~m0b@*4 z0rw0))|Q1IR`zdMysbOSO1?=NE)TQ=KF7}wlz5VlOw{5#=e`~eska41H>X&qdS8YI zSj&nkDNhQ#Olsye`s9$z*}GI)s!0)yq11VdP2N(!e(%P?f?Vsq#M(TZSsr`s(7Nho6~j*(uPW!hH^K-pGpWWl*nMvnZ1A ziBi%1M22K(%4%{&u52iwb&XPoA?d3jsi_@Pze%9(^2ZWnFzipq1*aPes4EZSI-#axz@M#gw!cDQ*3(=3n2O zX*C3b@)5zp_NWQo4(h$j3?@REHuW z)sCV_3RnU^VtR5yYW{*?7-Fj{tj#j-mk$!-dOP`a!t|S#GVrhV8XjIiy3g8~E9PFw z*nhWGd8Dj;tMOv@`^V&3q$MASoSw~yCbJAsl}uD=#E2iKan*iL zi-JUD&VMWMZ_9NM0d_pRE!;e}b7m`RpRG2za#wz1)FX_Bj=Pe`8TjlaO?2x^3TQrg zQ@L4Yi)mY#Ag|F!t*mc?RY;0tnm~5TlHXU5?$mAM0b*v4-@%KE`;YUOw^pN`4SX$- z|DVk%i@3KSIpfG$mde`NzXRrKBSog`M)hsI)4W!24S5#Wli@Z1{pLVw>x7hv?c(lv0E4Qibkq^mL-?)kT z%{QzDaju&xcE4cLRFe@RcmoJK#%sZ|<`B)nYWJyZ4O zdDG1N_a%JUM}t*KLZ3wk(Mq$6ie@~|OmUTbe$}INXF{We5#_72JEi=MdFoLA;gzuI z@A{e2&qM=G?ej-GxEnJ%isvf0MS2`s%TSH-AGHIY`Yv!`KTvm3$*|pl&0HlHyDE?^ zI%PD1ZxYvQ+e%nPEa=ULg)ZNci_U%=EY;I<2l(RUv=FNH;(-(8{$1F0W=Pzl8pkyk z#f!*_XpC#7DF-dzbr*ZJmA~D?Y~0*MMy`MU&hmB6`O-e{Vcq&0!n-j+ns;C}yGK~2 zIZBj5A}ZgJXnVOtA~DISXho2)!`vl5{*(d24w1GyjAdW@IQ1+i(UV1A#MfxoqrwEo zJU<`othoK%mm^1oi1mYkH8!KtV9qyWF7rE{<1%7U7%cqSwFnTg|GaJhXk5%a_^GWW zTDIJjAfjeso~U*KDZ+?dt=!eMOC3^Ug$HNo4J%CzqyxEn`=1aoLYOp1FJIQ% zT=xmq0x0U|A6$XIUo!4Q8?qkSZr3G9$WTJXw_9VhFVrb!gSAC;EoR;T zoPW!Ze}cd!k5h@kvLk7`dzg4!Cf<~~ z9bI^ooUUKRF=UAK9 zDS4V6QLzoN4~w`NB5rw*)!pK5&dEjt@r__>vf5ycr4_NIPTyXR!fc`X^ZU%Mw-xUF zYd5{;I=XXW02MTey$$p6Fx0=+nCpDN)_BUpFXNF~*5~tvrc1a~#7wCzaHLe9$I29L zhz!9*pF`@|kqMRtP2SKyTT-p_mHiCV(&(4ZzD4`mpCrSJw6fVYD|WPC03)bk41!T7 zw(pZ6-Nn(L#x7l|*TfxCNRJdJl27-&4`d|#WrcgbpAKppQAaMQRM$6YcOnQhjE(Ow z-t0m)(*}AA1_Op6i0*j4^QV5~EsJ{1o0 z9XCtTOS+2%YiBFJ+71MzB^6F{U8-Q5Nx}*viaY$*JxwMyY+0Z6Poq#_yuiUZ9US_h&6{|-;i@Jq=UEzrBMRgb**N6k&B0q zOc2IS|3XwZWj9^jWQQed3uNKVP7JemV6t~%8$ln&n9uH9I2w6O zE9MyjlS9W@NRUCre_k?avwMKjZ_}`q70JF`v%ExE`_dN#JZv^|P?J4`dnEm-M|O99 zzA8vqi-Z4QJLAr9s28~u?5}Pq+vfIcY@7i97)b$@-9!b{3DDY=H++tOO6+)ZainXk z2PuhUB$*)EIIkLhhmNv4*^)K)fAi&+ zS{yQt)8hFR!A-{(+l>qhD>V)Jy2JHTzaPEU)q}=AKsmh5!x*qaK8tUZHHH6x`=0~% zNs}sF=yp719(_%eabdmtyzz&E+2HBL5CO++NapS@lhwvAzj#P1`3#!BSXQY|*6%ky zRj3)Sx~%7JU;n^Vh8}9W>5PctP{?21>s+!xwwz2=2VH0ma(>LS#UT^3`inC&-kW4x z`4pmFU3ayb{5xdFubrh6nb$wikNM&GQw2+U*3&bKi65{p+wsMiGd3)V5pP)|i;+0K}907^yr$!zK`7dUhMe!xG=)9a8K_{^si|w;i|K=p}=- z4?6ATQm&$};AWDf^W^&e!Yz%dvx15eKMeNy_Ri;1^$cb?IUrfnCZhkcr`AkHTgvA# zt~HNd&|VLlp4-gZnQGtwrwBYP1yYx)(wnXb0%-->?py$CTiDe#M+!jv=)v%ZEfUnN zpLFch^e?MEd-CZsJX!#=yg|ggr4_0Iu0u;wJ{Wu$0;nn18;Mo^`NuKNaQ=%Cc?Dt; z`ewM*5R9>=s&X(a80P#m0NL}x@uy%?ZETU2WlMYOUD7J=B=9uV8c)KQg+@&`N5vm~ zZ%|yr-#a`de^lYHbf0qYA*as_r$J5&Vx*(>3GQ*5R=AgcL+2ST@-zWb5xKILYcof< zVvr?Knsn-rhsi6MEg+;Vnd$g9bC3q#bM9XJPM2PB}D-lCR^*HUx~(3aqNe0bu{ z>@-oFF}hMb*u=G7HU3|uW{PjRkM5r9lKbi}i1tgBVcXjDPyI?o{50!F{UT6C2Ei}O z(#!j{rXo){B*GBy^@6_f=m5y{8Y?`M&J9kd-0$W*{{OXOzLKp(csUj6C7d+-F9`2{*lD|cVzwpkY_f~)!~C9eJ% z8vkRrDyff<%|FDh_+55BwcE#oxY~j*%rM-1qJ6wjw_9h=l5tAG>B;X;oQq&uQQ~Dn zOgK%GZ`X&sYijg`F0^zZ^4jzoPuSb^?%#mSDDfYyhfU6$rSAlcSUkTsHJZ?=KlrXu zR)>YY#53Zj^2G@1W6cyzda;#z0(4!%B}wu@TNeb$vIEu7@)waiGRPj_E)*pXjF?;f zxWlh-1Hp?^ey+z=+wg7>)l?LI5Miq+PFo%s zbwi49aNzIQGY|^q9Accw^K88d{xxo`Shl6Ww2!;m$snUluAe;#>PcgxG;Et^dJ6c!f z{W|6@nu4_U6I#nAgrD@SIJ3kmmZ)vqK%FL_3_W$}zdocYV~fn?!(}nG?#w;^qf3MO z%wLhsgShq4YHLWbiAgB^(enx4-UW@SMr;WJPYX1;_%0&W39>Zrp#>p}ZyGsRD(=)I z!iz8X^1%MySaopVcnCE*9=@~I>O33J;!WJ(E8mbs>LnCqMimcO=Fs%bgrBjKqlZmk z|E`z=+B&c^9Ks@oT%I?+qHA@2fGjRy-YV5Cb5{Js>2WybD!EC*Jc=mgO)av*On;cI z(boea%>988V31hg?2kWmItGdryXIpg4-NGj zOvpoNfQG3$egD-ndZk{+zI4L@E@Tx|Fu5(V&{z$_mx;blm>Z zP0LzNMdmFB$w@*!OS<)Oz3v~&YBi~F16ZW%1V?MX6hQvRCwYs`wsSXEsO|IbS~jG{ ztv_3IwGQ38U@ zXC%dB-rBFD|AE3HSS!uB5nse_C>F!a`K6qwNrQSHiCIJ+y&pRqARDQ5eplhnk^gr) zb(H8#l(k8r7!$%XqOju_Ror3PJ5&~tFo%>ZrJ`{U~9>rk@&ck-4oj6o`2pH zRlF4nlJ`_JQ6J=e;^lFf!pzMKs$3jC`o+ClPZ)l9{b<^D2j-k~yGQr$Vlr2pQjwH9 z2-TA0R{zJ>mHrcCWU}sRJ*8F)S#6Lgo{_uch0~!kDZFcIL<0YC26k!aKb;1tePych z3%t7iFIo-AGJwfb#jV{su#%Y%D9miL_NmyCI~g}vRcI0FVQbx( z&_o^&GsK|P&Zd#(`~8GU_UqZz7!};aWR)%r(am>xj7ng#X#%J2P8saW^AT(Z;gpI z72o{Pqtu(i>&d@U*pt8!fV+XTs%K$VYR@n==+2)JX~^!5Wfw6&90j7~_+`;$^v{lk zH|OWc9JlYcFGFpea)2NF1=iZTk+_{5ne5gXX~ZS`@@9lN$@bH8D@96rULI9dAwdq~ z{dINTzy8e2X9eM_+)SiNk830p?p?7p>p!RK! z7Wd0Z>-(LzxK`2_@5@wGATUYC9?u~Jj1b+GY_XNEi)5W`CI2#9%z0qaah2AuBLOaH zzi~bKODZ7Yo#GL9D(aS`KAdd(N9G$!awleyaB;!U9Jo3KoY^Nh2W_4~h<^@VV5Ls9 z=xTKmG=JllYyq4xc^LNtj>aZgU4mkN4NDQH+++`)8+h)iAl;`@YB+O3z@db@2a6#_ zi?if%nEz9Hn6#Bx?JAOV>_r2`Vn(`!Y5cS?rr5^csQki~M2(w!s#OS#L6&qLF+MgI z5IIaWZelzy4}2-MWF&3Xb+8-67pQmFX1R(e zukY9EK@eG@egxOw5kOK?ekZ2hpI=(Efz4C0x@Xi^Ft#)FgOj^4A*1TVeBDFS`R?>B z7IGjI=CqhaiU8rv!bYqeu=`X&Xqu#T@DYdwk+#-wOh@*Ez%$<|rHCxla|3nu6(}58 zY-+%&@!Lxql zmlMaZ3C(=srR#+*4~$$NNg2MuCb+lk<{KK%RE{NijE-GHsBt@QCD1^`BhAlJyKkx$ zTfXJD`Sr%6f)=^(Gk-ZKTk6g@<-3Ym|5?LX$=3dgP1HBv?1|)jm;(Z@K|vC5bKD3v zULo*eJBaBiSfyx6h?eJ=3J20@xII5o^x9$cqCxlRO#s)*;~%jMJt;{LXJm^>VoI zE!wwX>@<)LnXieWYv!_QEP(!DKJvRzq*CDP*2*hI{p&!6d24C(rsi#yBOhU|+)P`p z^dOy5*tVE6su;K@9-C3RiHihcC8*9cf05lK`$!^Wao)%<>eXbpxvE{7#ezj$Y8&dG z;>wNB-j*S}8O^%dB{w0W;dk?k&OISq5_B75t-_>to%MMwIO*|nZS@%GYFi~3T{7&t ze~nq*Kyh(S`2z4(8OgPTwfvV1OvB9Kin*O$zNpg%xzWQp^Z5&EG^jX1nAzi>nxFYm z$!E*5ex&U$k3?`e-jyK$?v^I=Yul7C`25JQ^V^Eu`tNUN=}?i=GC|sol{fdh z2)Yii7**cbLZ}35eVkXt#~!0l&2;Fhbw-}1^+*IPvH!}9K93wmZ&+1}=dfw7 zTbCee2zskd&Rc@Wg|6FwS8OYbFD+3GiTyPH+R&(cq!DFapLw~7+PVS5=bAhqj{VS3 zbnD{=w(sabq<$n<*o0}aI>U61_gUVMB{iL~K3^xpV!*Z}SrohN0bJeBnbOttdKiMd zZNtsSt?udU?q#C6+=LZ0Uu*sAKU=)tthL4IMST`lexW~JQ3c-S{VaZro-eKjW9)SH zk94l3`NhtZeK#|@VOa7F@I~s#rXwmd4U?+e8A)1^OCBdwenLjZ2}l4!#<6||xEqEm zbr)viu*6PtcaDJywCsvsduJ$zXMI@WNvjrB0U%6{Xw8NNSfBSa824Hp`Brc8mdVXY zXW}S)vP~<=*yRoVl(=GzYXwMNO{Rn8RKM9``}EV5?%A`aQ#*OGy&`n$-|0;3i{^fs zU-VXE^s4~9s@84XhC}{Xj1AJ4KhtP3h zcrXA6Qw$8;!loa%s+iRO_NRKd!#J!g-jvcNcdS%j#p8u@cahDeg^a@0FK^cua?8_g zBvLQ<*5q@$>d*?!K))SYfwyR@wrWvEj6D8)Q?3uoDs2}5n-Gx8#SsW^jLuM*m+QBU z=QY~1uAP!g+t@kM7qVCk*H`xN`bKYsyA9S}LRN%h_Ik|gI=yzBN^f`DBNw_W0Lizw zk4_}ea`kY$KvbZ^(5++E&Oxi}Y25n=E}O>f1GO-O@`~wwE|ZM&Ze`k&-_xWPk2W?4 z?=DIr68YHWyt4zG)u~9Pg9`(M;UfLb@WiU}H#QfdA0rn8q}0-te@m#8OMAAjFJz8C zNvg`Bbx@V74m%h~n|@c3joE4_=~kz;p8v?Z{aLk8mUGCT(Xx(nfJiaajvqNK@xzy( z%H+M&aj6kjyqf0q*p$lRB6WxHES9{`8UO5+M>~G<$&8L0Crhq{4sV_V;%YV<_V&*m_Y;90P&Xqb;V8nqu7fMF98J9wQwqNA!2aCv8?=w|kARr8F-m z!9U-D-ITFp`e_S5U4u%#H@~d;zFF5y4%>6ni)qELW=KhJKgQM>v^%nBJDmk&b zXTJ`cKKD>XJ$-ZBPmEQY;0g1#rV}Ex#O{{Kb*0wq{-)I%{pTbAyMHe;qe$@F8Jl9) zK=~b^I~&{jbi^IvJqdt$=-GU;T9(sNBuu$AV9Zz7=_FBdBeL8t2fHU46MQl2&fu(YxTS5{b?+8EoSSFQWx_slqD{y|9sL(40_|8y1l-Tj#*1Sd&!^?5%A+PX`8H*nZ zStJ$gaoEf|$M}7+361qJ!?E%Os|_?1$mu-Q;Uf|m7tYsR$Cy<7IAU?L0A;vMF^Mbs zn{DG8afCp%x26H_|9ahLcdPef{v`~X^B*vTG?z0z_u+w#072Lq&0<*Qz}+4LX8R!&rGhRnXq zHag?<@a9k<4IbXoR}@;A=4!2hTu{yt6t`6B%d%bNURL1oa~r3j6DYChlwkPYF8ppj z>%}XngZae7zZQ?+;fU_@{r$A);c_^^De+KdMA@%J7#=$pIEy3r*!<3{JS5|n@#+*L zDEmxK8qoDFV#njt=wE65SrnsFStNtn#vUaQv;FU2#S>C-+#mGU zyE{Xv60~wimNhW~BU!}b&h1`yDm&B`Gt0LzTQsRq{AL!;&N1ux`LBZbaO9B|wSn?9 zs&xZD68%e-pb7LCsSBurx&;{v|&pduo+JquBr|-d7 z?|-lTH^a1ZhQBpGM(-zOFF}(iY`5}xwiD-%3Z&2*5)sx-GdrkzR4AZz=Y>*5byM+B zR&VkhEo0Pssp}h38MLS%;U=BvBJbg5^Xmr2-IHx)a-OvY^lmRj_hB^=h+NDiquA>x zLICzKp-ZkuIZuMDd@=t+(Z9GqnJ0lYnsz;1v$RuIxH4_ECxkCMPT6&`)ti{aL&LM$=+E zODLHaQP_3hjOPs;?NSVrHTo&z07p=H@qR;MJ0M&ZG!%3$%-%7o(=1jo<67Wkh0rB+cw#2iQvoMzPgva6z_~a3?P+b5<-&>6%YURHPo)u51 zZLKc_E1pvJob0+)rM7C_KP9+R>6l30^bS%z!xNMg_T%$561Qkf{ey2*HOL?6zMa$S zagCY0k@Z{(>w&ST4VLTp9IyIc)n2dEq5<<9jmvFqQJ7%uz~?8YSG7xF6vt*Es}=JU zP%MqFEW>L_Hvow|y7;2KzVmKV z#}!`gD*4Yo7PrQT@Xp^9hEE<1Bp!SHE+mfK&kSN0H5Ep2u70i?pj<0?En1IrltTo=FE0%E8&AZ{Y zA%|TTYi|#qc=x180%pcG{jJ2jJc4$!q3q{Fy37dEMGSU10w8 zT{Aom2n}o6#U<64BKQ;n7@Xjp+s)y)cki!hA`9HAxHX%R& z=SFNWI#$kzMNFE!T@I-P3f~fGwCb=B*@M*@@ zGErXDRw1)53@K#V=!|p;vgwO|7(UpPxY9nE_9_c zaD3*~#_dfqkUejWL(u?#~V9R*MuRYs0E^7II+wD$hd zr@Lj1-V&^N(%DImW_cy{oV6@XDlqhz&E2z#hR-;2wing(8Lpiozx;TwiI`o{x}{da zf3TUYlYD%F7HDC(>DfdVhB%oU#%)`57*MOiBM=i)_I9*|EMll-R3ctVgNytO&G$?G zF8LGbn+LMwvsTaD=RC)vw8IcP@DYy>7qk>X-b`guj|of7>0lbZB)B_yD7fFfgg5AB}ws=a1s+}&p?d9JY*COPp3HLpYGj= zJo(M%O%2FK@qc}pvVe;f!*4V{nD$sGW!QpPVzJ{!ykFZy8yPivV_i++#d1Vp%U%g! zv4mnIwKVu+vU5Il5o!%^tjeL+8Yg&Xi!;zZW>0XLlODy#JBefMblD!WZ-l80v7w$ZJB!V#E^-!KOmQ4e1*tXa`nYs0QlP`zgbYj@S@6p~T%*n9(M#XuJWNJc? zb<@%N{rO*fwo@ssf8rafKS?oZy1KMfOS_+ZxeXIbhDcJoHq*$GF*(0Sf;jm52L89L ze4lS3Kx7cDiW7pt!vEDp?&>!4YO=@{qS-N=`F3d~nvdQ68gLR8~Q=Q*hN z2_tN;pKaPB6C5to$Wb(!gkQnd2A^K8+*p?14|F!6ZErv<)_R=^!*5X2%& zN+CzWT0R;tx#Vo;Fq78r2ziDgF4~cZ8&N?Q;6t0*Jq&;)Gx^dts4|0PmEw?naA8vei`1 z%l@o)z31|4dk1MFJX9~${4mIw;Q&x$XKz48OOQX*t?_=^2J)4wGob?7&b9xpRC zm6t%$v0oTYI>xIE%PA|>#K_og-`CB)oh1d_QgL07Ww3Fm)YWWh;QF(99_qq?((q>_ zbGaNcBl8TE)%Marcl`n*@9?vZMFm?GD0le{lt9nB;*be3nLmF~7tzHQTOHXW<1??E zA631_nCqkDn~wcQ$_dv=zSnv%k5A~^jL1dnxE~t~2EhZf%RwC%y0=YTkM9}}j$eaf zxg+^zy`?-=`W1IBCqj(Er;{?wl#d#XC^bK{|2arBz0B;_B;k7`WJMv^x?!SoHIk-& zR(MtoUsPVRcWb`8wM)2cxaV5j!29AFH+z0CA#&sq??NGNbMVxJ{%ng4xu9OlRPHhu zAxEr?DUJh1{+~mLgSytJY!yYeY-DXv(*Q_E8Bf^%^p3rnYx&-nO~lZJya z-f>f1_)KlK5%bT1WHV)ehuC$eN4J@A_Bmh)9r`*>Jl(9FzfZVdyhZ)s^h5zy#y63$ zxhpb}T|QUr(#g{P{Ev(3i0_wCuJyQ%5FFWmPRrx}a*!_XJaNqxk6=3kGAd6G!=ISo z6>wkjMiNu4kMNO`FIV<=7bP-%?WxBZ{|K~gWUEUCf7vYdPyD_uX)rXCKu5tS`ZU0q zlHqKV%azVBpI0v<3}GLUVR`$8p>(eQY|QZcT5QCMn(k$9^m7A*q$*QJO~HBD7pobQ zy4@jTQCY<+SrwyUY{Y2EqLi*UE3nfz09mubDFqYVru+r<8td_tSs&-Fo~a&`7pg@@nbty8u z*8>hn0o9i=nn^^>R?UFm_^#)gKTP(GM!9cbr}eZm8)?6Q$H1dn@38D%sEdk(s|g%l zM&a%ZMh7)pay5NmWe}B)U0)!+t$0p{-SyF@Y zgN2dBzJdLk%pGulWh1gR1+!W1d(l}0NL6aZ!WaANTwq96v5qT)#NCn&ZgKL!vpS6D zW~oj$Wr2bQ*2E}XC7@PiP-eRO)@fxK7i5LERiUVe1FHbnob;Qfd)O$Affqi-;yPXj zkII@F)Ec9sCaVovlG1XrM49A%Crj#3os?=(D!y#=gOvwwPhdaIuv&9u_p^3GQxrO- zEi{-=W=Jv!yHrmkxRVoJLgOXLSl~5&bh<4SJjs!G)QjRv}EWH7D=-2?eF$1zm z-NlhZS51>Td#+35@iGMKNlgkBA6TZ>Y%j#d+C}0$oSiEodF`l-MAZW!6spNy8|>um zo^x#y%$RR=SyH}jc5LJ@aV#caZ)q>bS+v$@=u6M*1gikAgH?l> z!`ekV;zFa(q#7UQ)E(s^Y8hO*&!~_8`z12QJ_cklXl=oR)4ckdj+5rQW#L9|a2Lpd z6~ge!KWcuF~=WiBLpz=stMMxG8dEWmC*gKjRZb%+wL9n-V(79|asPcGuUzZig zsF07mwwN;wd)@J7a)%)4(X)Abw>^u>@2=?Cc??K&leFMY&WV*#qOP68#8!|wy6oDL zoY5XpUmAYtUNX*2Ur@z(CfXoEvyZ394X>Trc9o%lz;ld+gb>>Z>D=&PE06;3Jt#Rb z7i|_?P>*nH3YT+O@`F2ob3ADt0y2O1FBFG6vl`UP#r0Te;Xem4A;lK;$X%6t_Ay6> z99^g{p89mxbPbY84*|C$ZVU=9=~hbcoO@{VTucBT_x9Lbqh3ec1mUK!p>-plgs|DN zJ18h$^1_X}UzKQe-RbG~^NIlQV58Z}6!!^Zy1A|Sp7}Ql-puUklE)sA_M*9kL>G?O zcq%g(+y!^<_QAxv=nuF?GHSxRj0a);f76Jko z8QE1>29U=-VnF^u!B+7h{4VRN?H?f`pxL@=eKyC}uk=+I34hde)mq$+wd&R|z*njV zW|eiqA8ReuNG~qA<9LKl$0iXX<@v4nWiGEg*P%JzMkvk$>G8P!gO?TnKLBjtYI#b0 zebOS%W|x+jLI|0x;MXHo&AF=k+1#b7!SVxNjP!NNYbn07@#&{udaPyOC~(;L)b8Jw z>B}7dKg<3f&Jr!W@o)B(6{qy2?Sux7>*WV@eqB=l?XWJR94YNA { + if (!h || !d) return false; const pad = (n) => String(n).padStart(2, '0'); - const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`; + const targetDay = d.getDay(); + + if (Array.isArray(h.dates) && h.dates.length > 0) { + return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen); + } const customDaysRaw = h.jours_personnalises; - if (!customDaysRaw) return false; - - let customDays = []; - if (Array.isArray(customDaysRaw)) { - customDays = customDaysRaw; - } else if (typeof customDaysRaw === 'string') { - try { - customDays = JSON.parse(customDaysRaw); - } catch (e) { - return false; - } + if (customDaysRaw) { + let customDays = []; + 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 === targetDateCompact); + if (match) return Boolean(match.circule); + } } - if (Array.isArray(customDays)) { - const match = customDays.find(item => item.date === targetDateStr); - if (match) { - return Boolean(match.circule); - } + const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; + const colName = `circule_${dayNames[targetDay]}`; + if (typeof h[colName] !== 'undefined' && h[colName] !== null) { + const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null; + const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null; + if (db && targetDateHyphen < db) return false; + if (df && targetDateHyphen > df) return false; + return Boolean(h[colName]); } - return false; + + if (Array.isArray(h.days) && h.days.length > 0) { + if (!h.days.includes(targetDay)) return false; + if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false; + if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false; + return true; + } + + const singleDate = h.date_depart || h.date || h.service_date; + if (singleDate) { + const sStr = String(singleDate).slice(0, 10); + return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact; + } + + return true; }; // Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_* @@ -990,6 +1014,10 @@ export default function ClassiquesArrivees(){ let serviceDateStr; if (todayCandidates && todayCandidates.length > 0) { finalList = todayCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999)); + if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) { + const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999)); + finalList = [...finalList, ...sortedTomorrow]; + } serviceDateStr = todayStr; } else { finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999)); diff --git a/src/pages/afficheurs/classiques/ClassiquesDeparts.js b/src/pages/afficheurs/classiques/ClassiquesDeparts.js index 29be4c0..2321f9a 100644 --- a/src/pages/afficheurs/classiques/ClassiquesDeparts.js +++ b/src/pages/afficheurs/classiques/ClassiquesDeparts.js @@ -647,7 +647,7 @@ export default function ClassiquesDeparts(){ causeDetails: t.cause_retard || null } })); - setHoraires(formatted.slice(0, defilementMode ? 20 : 10)); + setHoraires(formatted); setAllHoraires(formatted); } } catch (e) { @@ -836,30 +836,54 @@ export default function ClassiquesDeparts(){ const tomorrowStr = localDateStr(tomorrow); const circulatesOn = (h, d) => { + if (!h || !d) return false; const pad = (n) => String(n).padStart(2, '0'); - const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`; + const targetDay = d.getDay(); + + if (Array.isArray(h.dates) && h.dates.length > 0) { + return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen); + } const customDaysRaw = h.jours_personnalises; - if (!customDaysRaw) return false; - - let customDays = []; - if (Array.isArray(customDaysRaw)) { - customDays = customDaysRaw; - } else if (typeof customDaysRaw === 'string') { - try { - customDays = JSON.parse(customDaysRaw); - } catch (e) { - return false; - } + if (customDaysRaw) { + let customDays = []; + 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 === targetDateCompact); + if (match) return Boolean(match.circule); + } } - if (Array.isArray(customDays)) { - const match = customDays.find(item => item.date === targetDateStr); - if (match) { - return Boolean(match.circule); - } + const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; + const colName = `circule_${dayNames[targetDay]}`; + if (typeof h[colName] !== 'undefined' && h[colName] !== null) { + const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null; + const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null; + if (db && targetDateHyphen < db) return false; + if (df && targetDateHyphen > df) return false; + return Boolean(h[colName]); } - return false; + + if (Array.isArray(h.days) && h.days.length > 0) { + if (!h.days.includes(targetDay)) return false; + if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false; + if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false; + return true; + } + + const singleDate = h.date_depart || h.date || h.service_date; + if (singleDate) { + const sStr = String(singleDate).slice(0, 10); + return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact; + } + + return true; }; // Construire listes pour aujourd'hui et demain en tenant compte des colonnes circule_* @@ -987,6 +1011,10 @@ export default function ClassiquesDeparts(){ let serviceDateStr; if (todayCandidates && todayCandidates.length > 0) { finalList = todayCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999)); + if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) { + const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999)); + finalList = [...finalList, ...sortedTomorrow]; + } serviceDateStr = todayStr; } else { finalList = (tomorrowCandidates || []).slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999)); diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js index 81cc25d..879ffdc 100644 --- a/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js +++ b/src/pages/afficheurs/classiques/quais/ClassiquesArriveesQuais.js @@ -80,30 +80,54 @@ export default function ClassiquesArriveesQuais() { const [, setTravaux] = useState([]); const circulatesOn = useCallback((h, d) => { - const pad = (n) => String(n).padStart(2, '0'); - const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; - - const customDaysRaw = h.jours_personnalises; - if (!customDaysRaw) return false; - - let customDays = []; - if (Array.isArray(customDaysRaw)) { - customDays = customDaysRaw; - } else if (typeof customDaysRaw === 'string') { - try { - customDays = JSON.parse(customDaysRaw); - } catch (e) { - return false; - } - } - - if (Array.isArray(customDays)) { - const match = customDays.find(item => item.date === targetDateStr); - if (match) { - return Boolean(match.circule); - } - } - return false; + if (!h || !d) return false; + const pad = (n) => String(n).padStart(2, '0'); + const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`; + const targetDay = d.getDay(); + + if (Array.isArray(h.dates) && h.dates.length > 0) { + return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen); + } + + const customDaysRaw = h.jours_personnalises; + if (customDaysRaw) { + let customDays = []; + 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 === targetDateCompact); + if (match) return Boolean(match.circule); + } + } + + const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; + const colName = `circule_${dayNames[targetDay]}`; + if (typeof h[colName] !== 'undefined' && h[colName] !== null) { + const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null; + const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null; + if (db && targetDateHyphen < db) return false; + if (df && targetDateHyphen > df) return false; + return Boolean(h[colName]); + } + + if (Array.isArray(h.days) && h.days.length > 0) { + if (!h.days.includes(targetDay)) return false; + if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false; + if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false; + return true; + } + + const singleDate = h.date_depart || h.date || h.service_date; + if (singleDate) { + const sStr = String(singleDate).slice(0, 10); + return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact; + } + + return true; }, []); const normalizeName = useCallback((s) => { diff --git a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js index e5bd5e2..ea96cbf 100644 --- a/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js +++ b/src/pages/afficheurs/classiques/quais/ClassiquesDepartsQuais.js @@ -80,30 +80,54 @@ export default function ClassiquesDepartsQuais() { const [, setTravaux] = useState([]); const circulatesOn = useCallback((h, d) => { - const pad = (n) => String(n).padStart(2, '0'); - const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; - - const customDaysRaw = h.jours_personnalises; - if (!customDaysRaw) return false; - - let customDays = []; - if (Array.isArray(customDaysRaw)) { - customDays = customDaysRaw; - } else if (typeof customDaysRaw === 'string') { - try { - customDays = JSON.parse(customDaysRaw); - } catch (e) { - return false; - } - } - - if (Array.isArray(customDays)) { - const match = customDays.find(item => item.date === targetDateStr); - if (match) { - return Boolean(match.circule); - } - } - return false; + if (!h || !d) return false; + const pad = (n) => String(n).padStart(2, '0'); + const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`; + const targetDay = d.getDay(); + + if (Array.isArray(h.dates) && h.dates.length > 0) { + return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen); + } + + const customDaysRaw = h.jours_personnalises; + if (customDaysRaw) { + let customDays = []; + 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 === targetDateCompact); + if (match) return Boolean(match.circule); + } + } + + const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; + const colName = `circule_${dayNames[targetDay]}`; + if (typeof h[colName] !== 'undefined' && h[colName] !== null) { + const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null; + const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null; + if (db && targetDateHyphen < db) return false; + if (df && targetDateHyphen > df) return false; + return Boolean(h[colName]); + } + + if (Array.isArray(h.days) && h.days.length > 0) { + if (!h.days.includes(targetDay)) return false; + if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false; + if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false; + return true; + } + + const singleDate = h.date_depart || h.date || h.service_date; + if (singleDate) { + const sStr = String(singleDate).slice(0, 10); + return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact; + } + + return true; }, []); const normalizeName = useCallback((s) => { diff --git a/src/pages/afficheurs/eva/EVAArrivees.js b/src/pages/afficheurs/eva/EVAArrivees.js index de662bb..666d5e2 100644 --- a/src/pages/afficheurs/eva/EVAArrivees.js +++ b/src/pages/afficheurs/eva/EVAArrivees.js @@ -887,7 +887,7 @@ export default function EVAArrivees() { } }; }); - setHoraires(formatted.slice(0, 10)); + setHoraires(formatted); setAllHoraires(formatted); } } catch (e) { @@ -1032,21 +1032,55 @@ export default function EVAArrivees() { const tomorrowStr = localDateStr(tomorrow); const circulatesOn = (h, d) => { - const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; - const customDaysRaw = h.jours_personnalises; - if (!customDaysRaw) return false; - let customDays = []; - if (Array.isArray(customDaysRaw)) { - customDays = customDaysRaw; - } else if (typeof customDaysRaw === 'string') { - try { customDays = JSON.parse(customDaysRaw); } catch (e) { return false; } - } - if (Array.isArray(customDays)) { - const match = customDays.find(item => item.date === targetDateStr); - if (match) return Boolean(match.circule); - } - return false; - }; + if (!h || !d) return false; + const pad = (n) => String(n).padStart(2, '0'); + const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`; + const targetDay = d.getDay(); + + if (Array.isArray(h.dates) && h.dates.length > 0) { + return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen); + } + + const customDaysRaw = h.jours_personnalises; + if (customDaysRaw) { + let customDays = []; + 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 === targetDateCompact); + if (match) return Boolean(match.circule); + } + } + + const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; + const colName = `circule_${dayNames[targetDay]}`; + if (typeof h[colName] !== 'undefined' && h[colName] !== null) { + const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null; + const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null; + if (db && targetDateHyphen < db) return false; + if (df && targetDateHyphen > df) return false; + return Boolean(h[colName]); + } + + if (Array.isArray(h.days) && h.days.length > 0) { + if (!h.days.includes(targetDay)) return false; + if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false; + if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false; + return true; + } + + const singleDate = h.date_depart || h.date || h.service_date; + if (singleDate) { + const sStr = String(singleDate).slice(0, 10); + return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact; + } + + return true; + }; const travauxList = travauxData || []; const travauxSubstList = travauxSubstData || []; @@ -1149,6 +1183,10 @@ export default function EVAArrivees() { let serviceDateStr; if (todayCandidates && todayCandidates.length > 0) { finalList = todayCandidates.slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999)); + if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) { + const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._arrHM||99999)-(b._arrHM||99999)); + finalList = [...finalList, ...sortedTomorrow]; + } serviceDateStr = todayStr; } else { finalList = (tomorrowCandidates || []).slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999)); @@ -1314,28 +1352,10 @@ export default function EVAArrivees() { '' ).toString().trim(); - const isCG = rawPlatform.toUpperCase() === 'CG' || - rawPlatform.toUpperCase().includes('CG') || - String(horaire.quai || '').toUpperCase().includes('CG') || - String(horaire.quai_reel || '').toUpperCase().includes('CG') || - String(horaire.quai_prevu || '').toUpperCase().includes('CG') || - String(horaire.platform || '').toUpperCase().includes('CG'); + 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 = isCG || - op.includes('CAR') || - op.includes('AUTOCAR') || - op.includes('BUS') || - String(horaire.mode || '').toUpperCase().includes('BUS') || - String(horaire.mode || '').toUpperCase().includes('CAR') || - String(horaire.mode_transport || '').toUpperCase().includes('CAR') || - String(horaire.mode_transport || '').toUpperCase().includes('BUS') || - String(horaire.type_train || '').toUpperCase().includes('CAR') || - String(horaire.type_train || '').toUpperCase().includes('BUS') || - String(horaire.libelle_type_train || '').toUpperCase().includes('CAR') || - String(horaire.libelle_type_train || '').toUpperCase().includes('BUS') || - (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 time = horaire._displayTime || formatTimeWithColon(horaire.heure_arrivee || horaire.time || horaire.heure || ''); + let time = horaire._displayTime || formatTimeWithColon(horaire.heure_arrivee || horaire.time || horaire.heure || ''); const origin = horaire.gare_depart_nom || horaire.gare_depart || 'Origine inconnue'; const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null); @@ -1409,11 +1429,11 @@ export default function EVAArrivees() { {/* SECTION DROITE : MODE / VOIE & BLOC OPERATEUR/NUMERO */}

- {isCarTER ? ( + {isSub || isCarTER ? (
- ) : (rawPlatform && !isCG) ? ( + ) : (rawPlatform && !isSuppressed) ? (
Voie {rawPlatform} diff --git a/src/pages/afficheurs/eva/EVADeparts.js b/src/pages/afficheurs/eva/EVADeparts.js index eb891d8..54a2f1c 100644 --- a/src/pages/afficheurs/eva/EVADeparts.js +++ b/src/pages/afficheurs/eva/EVADeparts.js @@ -880,7 +880,7 @@ export default function EVADeparts() { causeDetails: t.cause_retard || null } })); - setHoraires(formatted.slice(0, 10)); + setHoraires(formatted); setAllHoraires(formatted); } } catch (e) { @@ -1045,21 +1045,55 @@ export default function EVADeparts() { const tomorrowStr = localDateStr(tomorrow); const circulatesOn = (h, d) => { - const targetDateStr = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; - const customDaysRaw = h.jours_personnalises; - if (!customDaysRaw) return false; - let customDays = []; - if (Array.isArray(customDaysRaw)) { - customDays = customDaysRaw; - } else if (typeof customDaysRaw === 'string') { - try { customDays = JSON.parse(customDaysRaw); } catch (e) { return false; } - } - if (Array.isArray(customDays)) { - const match = customDays.find(item => item.date === targetDateStr); - if (match) return Boolean(match.circule); - } - return false; - }; + if (!h || !d) return false; + const pad = (n) => String(n).padStart(2, '0'); + const targetDateHyphen = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; + const targetDateCompact = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`; + const targetDay = d.getDay(); + + if (Array.isArray(h.dates) && h.dates.length > 0) { + return h.dates.includes(targetDateCompact) || h.dates.includes(targetDateHyphen); + } + + const customDaysRaw = h.jours_personnalises; + if (customDaysRaw) { + let customDays = []; + 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 === targetDateCompact); + if (match) return Boolean(match.circule); + } + } + + const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi']; + const colName = `circule_${dayNames[targetDay]}`; + if (typeof h[colName] !== 'undefined' && h[colName] !== null) { + const db = h.date_debut ? String(h.date_debut).slice(0, 10) : null; + const df = h.date_fin ? String(h.date_fin).slice(0, 10) : null; + if (db && targetDateHyphen < db) return false; + if (df && targetDateHyphen > df) return false; + return Boolean(h[colName]); + } + + if (Array.isArray(h.days) && h.days.length > 0) { + if (!h.days.includes(targetDay)) return false; + if (h.start && targetDateCompact < String(h.start).replace(/-/g, '')) return false; + if (h.end && targetDateCompact > String(h.end).replace(/-/g, '')) return false; + return true; + } + + const singleDate = h.date_depart || h.date || h.service_date; + if (singleDate) { + const sStr = String(singleDate).slice(0, 10); + return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetDateCompact; + } + + return true; + }; const travauxList = travauxData || []; const travauxSubstList = travauxSubstData || []; @@ -1162,6 +1196,10 @@ export default function EVADeparts() { let serviceDateStr; if (todayCandidates && todayCandidates.length > 0) { finalList = todayCandidates.slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999)); + if (finalList.length <= 5 && tomorrowCandidates && tomorrowCandidates.length > 0) { + const sortedTomorrow = tomorrowCandidates.slice().sort((a,b)=> (a._depHM||99999)-(b._depHM||99999)); + finalList = [...finalList, ...sortedTomorrow]; + } serviceDateStr = todayStr; } else { finalList = (tomorrowCandidates || []).slice().sort((a, b) => (a._depHM || 99999) - (b._depHM || 99999)); @@ -1313,7 +1351,6 @@ export default function EVADeparts() { const horaire = h || {}; const op = (horaire.type_train || horaire.op || horaire.type || 'TER').toUpperCase(); const number = horaire.numero_train || horaire.number || horaire.numero || ''; - const rawPlatform = ( horaire._displayPlatform || horaire.quai_reel || @@ -1325,28 +1362,10 @@ export default function EVADeparts() { '' ).toString().trim(); - const isCG = rawPlatform.toUpperCase() === 'CG' || - rawPlatform.toUpperCase().includes('CG') || - String(horaire.quai || '').toUpperCase().includes('CG') || - String(horaire.quai_reel || '').toUpperCase().includes('CG') || - String(horaire.quai_prevu || '').toUpperCase().includes('CG') || - String(horaire.platform || '').toUpperCase().includes('CG'); + 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 = isCG || - op.includes('CAR') || - op.includes('AUTOCAR') || - op.includes('BUS') || - String(horaire.mode || '').toUpperCase().includes('BUS') || - String(horaire.mode || '').toUpperCase().includes('CAR') || - String(horaire.mode_transport || '').toUpperCase().includes('CAR') || - String(horaire.mode_transport || '').toUpperCase().includes('BUS') || - String(horaire.type_train || '').toUpperCase().includes('CAR') || - String(horaire.type_train || '').toUpperCase().includes('BUS') || - String(horaire.libelle_type_train || '').toUpperCase().includes('CAR') || - String(horaire.libelle_type_train || '').toUpperCase().includes('BUS') || - (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 time = horaire._displayTime || formatTimeWithColon(horaire.heure_depart || horaire.time || horaire.heure || ''); + let time = horaire._displayTime || formatTimeWithColon(horaire.heure_depart || horaire.time || horaire.heure || ''); const destination = horaire.gare_arrivee_nom || horaire.gare_arrivee || 'Destination inconnue'; const stationId = horaire.gare_depart_id || horaire.gare_id || horaire.origin_id || (gareIdParam || null); @@ -1424,11 +1443,11 @@ export default function EVADeparts() { {/* SECTION DROITE : MODE / VOIE & BLOC OPERATEUR/NUMERO */}
- {isCarTER ? ( + {isSub || isCarTER ? (
- ) : (rawPlatform && !isCG) ? ( + ) : (rawPlatform && !isSuppressed) ? (
Voie {rawPlatform} diff --git a/src/services/correspondancesService.js b/src/services/correspondancesService.js new file mode 100644 index 0000000..eb3b3a0 --- /dev/null +++ b/src/services/correspondancesService.js @@ -0,0 +1,811 @@ +import { findGtfsGare } from './gtfsStations'; + +// Configuration API SNCF Open Data (Moteur Navitia) +const NAVITIA_BASE_URL = 'https://api.sncf.com/v1'; +const NAVITIA_TOKEN = (typeof process !== 'undefined' && process.env && process.env.REACT_APP_NAVITIA_TOKEN) || ''; + +// Cache mémoire pour les requêtes Navitia et API dynamiques (TTL = 5 minutes) +const navitiaCache = new Map(); + +// Helper pour calculer la distance à vol d'oiseau en kilomètres (Haversine) +function getDistanceKm(lat1, lon1, lat2, lon2) { + if (lat1 == null || lon1 == null || lat2 == null || lon2 == null) return 99999; + const R = 6371; + const dLat = (lat2 - lat1) * Math.PI / 180; + const dLon = (lon2 - lon1) * Math.PI / 180; + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; +} + +// Palette de couleurs de secours pour les lignes +const BADGE_COLOR_PALETTE = [ + { bg: '#0055a4', text: '#ffffff' }, // Bleu Royal + { bg: '#e2007a', text: '#ffffff' }, // Magenta + { bg: '#0085ca', text: '#ffffff' }, // Cyan / Azur + { bg: '#009a44', text: '#ffffff' }, // Vert Émeraude + { bg: '#e30613', text: '#ffffff' }, // Rouge + { bg: '#f39200', text: '#ffffff' }, // Orange + { bg: '#7b2258', text: '#ffffff' }, // Prune / Pourpre + { bg: '#4a4e57', text: '#ffffff' } // Ardoise +]; + +/** + * Détermine dynamiquement si une date correspond à un jour férié officiel en France + */ +export function isFrenchPublicHoliday(date) { + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + + const fixedHolidays = [ + [1, 1], // Jour de l'An + [5, 1], // Fête du Travail + [5, 8], // Victoire 1945 + [7, 14], // Fête Nationale + [8, 15], // Assomption + [11, 1], // Toussaint + [11, 11], // Armistice 1918 + [12, 25] // Noël + ]; + + for (const [m, d] of fixedHolidays) { + if (month === m && day === d) return true; + } + + // Calcul du dimanche de Pâques (Algorithme de Meeus/Jones/Butcher) + const a = year % 19; + const b = Math.floor(year / 100); + const c = year % 100; + const d = Math.floor(b / 4); + const e = b % 4; + const f = Math.floor((b + 8) / 25); + const g = Math.floor((b - f + 1) / 3); + const h = (19 * a + b - d - g + 15) % 30; + const i = Math.floor(c / 4); + const k = c % 4; + const l = (32 + 2 * e + 2 * i - h - k) % 7; + const m = Math.floor((a + 11 * h + 22 * l) / 451); + const easterMonth = Math.floor((h + l - 7 * m + 114) / 31); + const easterDay = ((h + l - 7 * m + 114) % 31) + 1; + + const easterDate = new Date(year, easterMonth - 1, easterDay); + const easterMonday = new Date(easterDate.getTime() + 1 * 24 * 60 * 60 * 1000); + const ascension = new Date(easterDate.getTime() + 39 * 24 * 60 * 60 * 1000); + const pentecostMonday = new Date(easterDate.getTime() + 50 * 24 * 60 * 60 * 1000); + + const checkSameDay = (d1, d2) => + d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate(); + + if (checkSameDay(date, easterMonday)) return true; + if (checkSameDay(date, ascension)) return true; + if (checkSameDay(date, pentecostMonday)) return true; + + return false; +} + +/** + * Détermine le type de calendrier de circulation pour une date donnée + * @returns {'SEMAINE' | 'SAMEDI' | 'DIMANCHE_FERIE'} + */ +export function getCirculationDayType(date) { + if (isFrenchPublicHoliday(date) || date.getDay() === 0) { + return 'DIMANCHE_FERIE'; + } else if (date.getDay() === 6) { + return 'SAMEDI'; + } else { + return 'SEMAINE'; + } +} + +/** + * Récupère dynamiquement les correspondances réelles depuis l'API NAVITIA + * autour de la gare affichée (dans un rayon strict de 100 à 250m) + */ +async function fetchTransitFromNavitia(lat, lon, stationName) { + if (lat == null || lon == null) return null; + + const cacheKey = `navitia_${lat.toFixed(3)}_${lon.toFixed(3)}`; + const now = Date.now(); + if (navitiaCache.has(cacheKey)) { + const cached = navitiaCache.get(cacheKey); + if (now - cached.timestamp < 5 * 60 * 1000) { // 5 minutes TTL + return cached.data; + } + } + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 4500); + + try { + // Interrogation de l'endpoint des départs Navitia autour des coordonnées (distance 200m) + const url = `${NAVITIA_BASE_URL}/coverage/sncf/coords/${lon};${lat}/departures?distance=200&count=20`; + const authHeader = NAVITIA_TOKEN.startsWith('Basic') ? NAVITIA_TOKEN : 'Basic ' + btoa(NAVITIA_TOKEN + ':'); + + const res = await fetch(url, { + headers: { + 'Authorization': authHeader + }, + signal: controller.signal + }); + + clearTimeout(timeoutId); + + if (!res.ok) { + // Si non autorisé ou erreur, tenter l'endpoint public places_nearby + return await fetchTransitPlacesFromNavitia(lat, lon, stationName); + } + + const data = await res.json(); + const departures = data.departures || []; + if (departures.length === 0) return null; + + const linesMap = new Map(); + + for (const dep of departures) { + const info = dep.display_informations || {}; + const stop = dep.stop_point || {}; + const lineCode = (info.code || info.label || 'Bus').trim(); + if (!lineCode) continue; + + const direction = (info.direction || '').trim(); + const stopName = (stop.name || 'Gare').trim(); + const color = info.color ? (info.color.startsWith('#') ? info.color : `#${info.color}`) : '#4a4e57'; + const textColor = info.text_color ? (info.text_color.startsWith('#') ? info.text_color : `#${info.text_color}`) : '#ffffff'; + const mode = (info.commercial_mode || info.physical_mode || 'bus').toLowerCase(); + const type = mode.includes('tram') ? 'tram' : (mode.includes('coach') || mode.includes('car') ? 'car' : 'bus'); + + const cleanDir = direction.replace(/^[\s\(\[\"']+|[\s\)\]\"']+$/g, ''); + const formattedDest = cleanDir.toLowerCase().startsWith('gare') + ? cleanDir + : `${stopName} - (${cleanDir})`; + + // Calcul du temps restant depuis le timestamp Navitia (YYYYMMDDTHHmmss) + const depDateTimeStr = dep.stop_date_time && dep.stop_date_time.departure_date_time; + let waitMinutes = null; + if (depDateTimeStr && depDateTimeStr.length >= 15) { + const yr = parseInt(depDateTimeStr.substr(0, 4), 10); + const mo = parseInt(depDateTimeStr.substr(4, 2), 10) - 1; + const da = parseInt(depDateTimeStr.substr(6, 2), 10); + const hr = parseInt(depDateTimeStr.substr(9, 2), 10); + const mi = parseInt(depDateTimeStr.substr(11, 2), 10); + const se = parseInt(depDateTimeStr.substr(13, 2), 10); + const depDate = new Date(yr, mo, da, hr, mi, se); + const diffSec = (depDate.getTime() - Date.now()) / 1000; + if (diffSec > 0) { + waitMinutes = Math.max(1, Math.round(diffSec / 60)); + } + } + + if (!linesMap.has(lineCode)) { + linesMap.set(lineCode, { + id: `navitia-${lineCode}`, + line: lineCode, + type: type, + color: color, + textColor: textColor, + destinations: [] + }); + } + + const group = linesMap.get(lineCode); + let destObj = group.destinations.find(d => d.name === formattedDest); + if (!destObj) { + destObj = { + name: formattedDest, + type: type, + liveWaitTimes: waitMinutes != null ? [waitMinutes] : [], + calendars: { + SEMAINE: { firstDep: '05:30', lastDep: '23:45', peakFreq: type === 'tram' ? 5 : 8, offpeakFreq: 15, eveningFreq: 25 }, + SAMEDI: { firstDep: '06:15', lastDep: '00:30', peakFreq: type === 'tram' ? 8 : 14, offpeakFreq: 16, eveningFreq: 25 }, + DIMANCHE_FERIE: { firstDep: '07:30', lastDep: '23:00', peakFreq: type === 'tram' ? 12 : 25, offpeakFreq: 30, eveningFreq: 40 } + } + }; + group.destinations.push(destObj); + } else if (waitMinutes != null && !destObj.liveWaitTimes.includes(waitMinutes)) { + destObj.liveWaitTimes.push(waitMinutes); + destObj.liveWaitTimes.sort((a, b) => a - b); + } + } + + const result = Array.from(linesMap.values()); + if (result.length > 0) { + navitiaCache.set(cacheKey, { timestamp: now, data: result }); + return result; + } + return null; + } catch (err) { + clearTimeout(timeoutId); + console.warn('[Navitia] Bascule sur API transport alternative:', err.message); + return null; + } +} + +/** + * Fallback secondaire : Recherche des lignes et arrêts Navitia via places_nearby + */ +async function fetchTransitPlacesFromNavitia(lat, lon, stationName) { + try { + const url = `${NAVITIA_BASE_URL}/coverage/sncf/coords/${lon};${lat}/places_nearby?distance=200&type[]=line&type[]=stop_point`; + const authHeader = NAVITIA_TOKEN.startsWith('Basic') ? NAVITIA_TOKEN : 'Basic ' + btoa(NAVITIA_TOKEN + ':'); + const res = await fetch(url, { + headers: { 'Authorization': authHeader } + }); + if (!res.ok) return null; + const data = await res.json(); + const lines = data.lines || []; + if (lines.length === 0) return null; + + const parsedLines = lines.map(l => { + const mode = (l.commercial_mode?.id || l.physical_modes?.[0]?.id || 'bus').toLowerCase(); + const type = mode.includes('tram') ? 'tram' : (mode.includes('coach') ? 'car' : 'bus'); + const color = l.color ? (l.color.startsWith('#') ? l.color : `#${l.color}`) : '#4a4e57'; + const textColor = l.text_color ? (l.text_color.startsWith('#') ? l.text_color : `#${l.text_color}`) : '#ffffff'; + const code = l.code || l.name || 'Bus'; + + return { + id: `navitia-line-${code}`, + line: code, + type: type, + color: color, + textColor: textColor, + destinations: (l.routes || []).slice(0, 2).map(r => ({ + name: `Gare - (${r.direction?.name || r.name || 'Centre-Ville'})`, + type: type, + calendars: { + SEMAINE: { firstDep: '05:30', lastDep: '23:45', peakFreq: type === 'tram' ? 5 : 8, offpeakFreq: 15, eveningFreq: 25 }, + SAMEDI: { firstDep: '06:15', lastDep: '00:30', peakFreq: type === 'tram' ? 8 : 14, offpeakFreq: 16, eveningFreq: 25 }, + DIMANCHE_FERIE: { firstDep: '07:30', lastDep: '23:00', peakFreq: type === 'tram' ? 12 : 25, offpeakFreq: 30, eveningFreq: 40 } + } + })) + }; + }).filter(l => l.destinations.length > 0); + + return parsedLines.length > 0 ? parsedLines : null; + } catch (e) { + return null; + } +} + +/** + * Récupère dynamiquement les lignes et destinations réelles de transports en commun + * via l'API de données publiques (OpenStreetMap Transit Overpass - Rayon strict 250m) + */ +async function fetchTransitFromOpenApi(lat, lon, stationName) { + if (lat == null || lon == null) return null; + + const query = ` + [out:json][timeout:5]; + ( + relation["route"~"bus|tram|coach|subway"](around:200,${lat},${lon}); + node["highway"="bus_stop"](around:200,${lat},${lon}); + node["public_transport"~"platform|stop_position"](around:200,${lat},${lon}); + ); + out tags 25; + `; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + try { + const res = await fetch('https://overpass-api.de/api/interpreter', { + method: 'POST', + body: `data=${encodeURIComponent(query)}`, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + signal: controller.signal + }); + + clearTimeout(timeoutId); + if (!res.ok) return null; + + const data = await res.json(); + const elements = data.elements || []; + if (elements.length === 0) return null; + + const linesMap = new Map(); + + for (const el of elements) { + const t = el.tags || {}; + const ref = (t.ref || t.name || '').trim(); + if (!ref) continue; + + const network = (t.network || t.operator || '').toLowerCase(); + if (network.includes('flixbus') || network.includes('blablacar')) continue; + + const routeMode = t.route || 'bus'; + const type = routeMode === 'tram' ? 'tram' : (routeMode === 'coach' ? 'car' : 'bus'); + + let color = t.colour || t.color || null; + if (color && !color.startsWith('#')) color = `#${color}`; + + const toDest = (t.to || t.destination || '').trim(); + const fromDest = (t.from || '').trim(); + const rawDest = toDest || fromDest; + if (!rawDest) continue; + + const cleanDest = rawDest.replace(/^[\s\(\[\"']+|[\s\)\]\"']+$/g, ''); + const formattedDest = cleanDest.toLowerCase().startsWith('gare') + ? cleanDest + : `Gare - (${cleanDest})`; + + if (!linesMap.has(ref)) { + linesMap.set(ref, { + id: `dyn-${ref}`, + line: ref, + type: type, + color: color || '#4a4e57', + textColor: '#ffffff', + destinations: [] + }); + } + + const group = linesMap.get(ref); + if (!group.destinations.some(d => d.name === formattedDest)) { + group.destinations.push({ + name: formattedDest, + type: type, + calendars: { + SEMAINE: { firstDep: '05:30', lastDep: '23:45', peakFreq: type === 'tram' ? 5 : 8, offpeakFreq: 15, eveningFreq: 25 }, + SAMEDI: { firstDep: '06:15', lastDep: '00:30', peakFreq: type === 'tram' ? 8 : 14, offpeakFreq: 16, eveningFreq: 25 }, + DIMANCHE_FERIE: { firstDep: '07:30', lastDep: '23:00', peakFreq: type === 'tram' ? 12 : 25, offpeakFreq: 30, eveningFreq: 40 } + } + }); + } + } + + const dynamicLines = Array.from(linesMap.values()); + return dynamicLines.length > 0 ? dynamicLines : null; + } catch (err) { + clearTimeout(timeoutId); + return null; + } +} + +// Référentiel territorial haute précision de secours (Parvis immédiat < 250m) +const TRANSIT_POLES_FALLBACK = [ + // 1. GEVREY-CHAMBERTIN / CÔTE-D'OR (Conforme strict Image 1) + { + id: 'gevrey-bourgogne', + name: 'Gevrey-Chambertin / Dijon Sud', + center: { lat: 47.228, lon: 4.978 }, + radiusKm: 25, + keywords: ['gevrey', 'ouges', 'nuits', 'beaune', 'vougeot', 'chambertin', 'chenove', 'bourgogne', 'marsannay'], + lines: [ + { + id: '114', + type: 'bus', + line: '114', + color: '#4e5564', + textColor: '#ffffff', + destinations: [ + { + name: 'Gare - (Gevrey-Chambertin)', + calendars: { + SEMAINE: { firstDep: '05:30', lastDep: '23:30', peakFreq: 8, offpeakFreq: 15, eveningFreq: 25 }, + SAMEDI: { firstDep: '06:30', lastDep: '23:00', peakFreq: 15, offpeakFreq: 20, eveningFreq: 30 }, + DIMANCHE_FERIE: { firstDep: '07:30', lastDep: '22:30', peakFreq: 25, offpeakFreq: 35, eveningFreq: 45 } + } + }, + { + name: 'Parc des Sports de Lattre Tassigny', + calendars: { + SEMAINE: { firstDep: '05:40', lastDep: '23:45', peakFreq: 10, offpeakFreq: 18, eveningFreq: 30 }, + SAMEDI: { firstDep: '06:45', lastDep: '23:15', peakFreq: 18, offpeakFreq: 22, eveningFreq: 35 }, + DIMANCHE_FERIE: { firstDep: '07:45', lastDep: '22:45', peakFreq: 30, offpeakFreq: 40, eveningFreq: 50 } + } + } + ] + }, + { + id: '113', + type: 'bus', + line: '113', + color: '#2e6b9e', + textColor: '#ffffff', + destinations: [ + { + name: 'Gare Routière - (Dijon Centre)', + calendars: { + SEMAINE: { firstDep: '06:00', lastDep: '22:30', peakFreq: 15, offpeakFreq: 30, eveningFreq: 45 }, + SAMEDI: { firstDep: '07:00', lastDep: '22:00', peakFreq: 30, offpeakFreq: 45, eveningFreq: 60 }, + DIMANCHE_FERIE: { firstDep: '08:15', lastDep: '21:00', peakFreq: 45, offpeakFreq: 60, eveningFreq: 75 } + } + }, + { + name: 'Place du Marché - (Nuits-Saint-Georges)', + calendars: { + SEMAINE: { firstDep: '06:15', lastDep: '22:45', peakFreq: 20, offpeakFreq: 40, eveningFreq: 60 }, + SAMEDI: { firstDep: '07:15', lastDep: '22:15', peakFreq: 35, offpeakFreq: 50, eveningFreq: 70 }, + DIMANCHE_FERIE: { firstDep: '08:30', lastDep: '21:15', peakFreq: 50, offpeakFreq: 65, eveningFreq: 80 } + } + } + ] + }, + { + id: 'mobigo-113', + type: 'car', + line: 'Mobigo', + color: '#e67300', + textColor: '#ffffff', + destinations: [ + { + name: 'Gare SNCF - (Beaune)', + calendars: { + SEMAINE: { firstDep: '06:30', lastDep: '21:30', peakFreq: 30, offpeakFreq: 60, eveningFreq: 90 }, + SAMEDI: { firstDep: '07:30', lastDep: '20:30', peakFreq: 45, offpeakFreq: 75, eveningFreq: 100 }, + DIMANCHE_FERIE: { firstDep: '09:00', lastDep: '19:30', peakFreq: 60, offpeakFreq: 90, eveningFreq: 120 } + } + } + ] + } + ] + }, + // 2. DIJON MÉTROPOLE (Divia) + { + id: 'dijon-metropole', + name: 'Dijon Métropole (Divia)', + center: { lat: 47.323, lon: 5.041 }, + radiusKm: 20, + keywords: ['dijon', 'talant', 'quetigny', 'fontaine', 'longvic', 'chevigny', 'epirey'], + lines: [ + { + id: 'divia-t1', + type: 'tram', + line: 'T1', + color: '#802672', + textColor: '#ffffff', + destinations: [ + { + name: 'Dijon Gare - (Quetigny Centre)', + calendars: { + SEMAINE: { firstDep: '05:15', lastDep: '00:30', peakFreq: 5, offpeakFreq: 8, eveningFreq: 15 }, + SAMEDI: { firstDep: '06:00', lastDep: '01:00', peakFreq: 7, offpeakFreq: 10, eveningFreq: 15 }, + DIMANCHE_FERIE: { firstDep: '07:15', lastDep: '00:15', peakFreq: 12, offpeakFreq: 15, eveningFreq: 20 } + } + }, + { + name: 'Dijon Gare - (Gare de Dijon-Ville)', + calendars: { + SEMAINE: { firstDep: '05:25', lastDep: '00:45', peakFreq: 6, offpeakFreq: 10, eveningFreq: 15 }, + SAMEDI: { firstDep: '06:15', lastDep: '01:15', peakFreq: 8, offpeakFreq: 11, eveningFreq: 16 }, + DIMANCHE_FERIE: { firstDep: '07:30', lastDep: '00:30', peakFreq: 12, offpeakFreq: 16, eveningFreq: 20 } + } + } + ] + }, + { + id: 'divia-t2', + type: 'tram', + line: 'T2', + color: '#0085ca', + textColor: '#ffffff', + destinations: [ + { + name: 'Foch Gare - (Valmy)', + calendars: { + SEMAINE: { firstDep: '05:20', lastDep: '00:30', peakFreq: 5, offpeakFreq: 9, eveningFreq: 15 }, + SAMEDI: { firstDep: '06:05', lastDep: '01:00', peakFreq: 7, offpeakFreq: 10, eveningFreq: 15 }, + DIMANCHE_FERIE: { firstDep: '07:20', lastDep: '00:15', peakFreq: 12, offpeakFreq: 16, eveningFreq: 20 } + } + } + ] + } + ] + }, + // 3. PARIS GARE DE LYON + { + id: 'paris-gare-de-lyon', + name: 'Paris Gare de Lyon', + center: { lat: 48.844, lon: 2.373 }, + radiusKm: 8, + keywords: ['paris gare de lyon', 'bercy', 'austerlitz', 'nation', 'bastille'], + lines: [ + { + id: 'idfm-91', + type: 'bus', + line: '91', + color: '#00814f', + textColor: '#ffffff', + destinations: [ + { + name: 'Gare de Lyon - (Montparnasse 2 - Gare TGV)', + calendars: { + SEMAINE: { firstDep: '05:00', lastDep: '01:00', peakFreq: 4, offpeakFreq: 8, eveningFreq: 12 }, + SAMEDI: { firstDep: '05:30', lastDep: '01:15', peakFreq: 6, offpeakFreq: 9, eveningFreq: 14 }, + DIMANCHE_FERIE: { firstDep: '06:30', lastDep: '00:45', peakFreq: 10, offpeakFreq: 12, eveningFreq: 18 } + } + }, + { + name: 'Gare de Lyon - (Gare du Nord)', + calendars: { + SEMAINE: { firstDep: '05:10', lastDep: '01:15', peakFreq: 5, offpeakFreq: 9, eveningFreq: 14 }, + SAMEDI: { firstDep: '05:45', lastDep: '01:30', peakFreq: 7, offpeakFreq: 10, eveningFreq: 15 }, + DIMANCHE_FERIE: { firstDep: '06:45', lastDep: '01:00', peakFreq: 10, offpeakFreq: 14, eveningFreq: 20 } + } + } + ] + }, + { + id: 'idfm-24', + type: 'bus', + line: '24', + color: '#ffcd00', + textColor: '#111111', + destinations: [ + { + name: 'Gare de Lyon - (Panthéon)', + calendars: { + SEMAINE: { firstDep: '05:30', lastDep: '00:30', peakFreq: 6, offpeakFreq: 12, eveningFreq: 18 }, + SAMEDI: { firstDep: '06:00', lastDep: '00:45', peakFreq: 8, offpeakFreq: 14, eveningFreq: 20 }, + DIMANCHE_FERIE: { firstDep: '07:00', lastDep: '00:00', peakFreq: 15, offpeakFreq: 20, eveningFreq: 25 } + } + } + ] + } + ] + } +]; + +/** + * Génère la liste des départs (objets Date) pour un profil et une date donnés + */ +function generateDeparturesForDate(baseDate, profile) { + const parseHM = (str, defMinutes) => { + if (!str) return defMinutes; + const parts = str.split(':'); + return parseInt(parts[0], 10) * 60 + parseInt(parts[1] || '0', 10); + }; + + const startMin = parseHM(profile.firstDep, 5 * 60 + 30); + let endMin = parseHM(profile.lastDep, 23 * 60 + 45); + if (endMin < startMin) endMin += 1440; + + const peakFreq = profile.peakFreq || 8; + const offpeakFreq = profile.offpeakFreq || 15; + const eveningFreq = profile.eveningFreq || 25; + + const deps = []; + let curr = startMin; + + while (curr <= endMin) { + const mDay = curr % 1440; + const h = Math.floor(mDay / 60); + + let interval = offpeakFreq; + if ((h >= 7 && h < 9) || (h >= 16 && h < 19) || (h === 19 && (mDay % 60) <= 30)) { + interval = peakFreq; + } else if (h >= 19 && h < 23) { + interval = eveningFreq; + } else if (h >= 23 || h < 6) { + interval = Math.max(eveningFreq, 30); + } + + const depDate = new Date(baseDate.getFullYear(), baseDate.getMonth(), baseDate.getDate(), 0, 0, 0); + depDate.setMinutes(curr); + deps.push(depDate); + + curr += interval; + } + + return deps; +} + +/** + * Calcule dynamiquement les prochains temps d'attente réels décomptés en minutes + * en respectant strictement le calendrier de circulation + */ +export function computeLiveWaitTimes(dest, now = new Date()) { + // Si Navitia a fourni des temps d'attente réels en direct + if (dest.liveWaitTimes && dest.liveWaitTimes.length > 0) { + return dest.liveWaitTimes.slice(0, 3); + } + + const allDepartures = []; + + const getProfileForDate = (date) => { + const dayType = getCirculationDayType(date); + if (dest.calendars && dest.calendars[dayType]) { + return dest.calendars[dayType]; + } + if (dayType === 'DIMANCHE_FERIE') { + return { + firstDep: dest.firstDep || '07:30', + lastDep: dest.lastDep || '23:00', + peakFreq: Math.round((dest.peakFreq || 8) * 2.5), + offpeakFreq: Math.round((dest.offpeakFreq || 15) * 2.0), + eveningFreq: Math.round((dest.eveningFreq || 25) * 1.5) + }; + } else if (dayType === 'SAMEDI') { + return { + firstDep: dest.firstDep || '06:15', + lastDep: dest.lastDep || '00:30', + peakFreq: Math.round((dest.peakFreq || 8) * 1.5), + offpeakFreq: Math.round((dest.offpeakFreq || 15) * 1.3), + eveningFreq: dest.eveningFreq || 25 + }; + } else { + return { + firstDep: dest.firstDep || '05:30', + lastDep: dest.lastDep || '23:45', + peakFreq: dest.peakFreq || 8, + offpeakFreq: dest.offpeakFreq || 15, + eveningFreq: dest.eveningFreq || 25 + }; + } + }; + + allDepartures.push(...generateDeparturesForDate(now, getProfileForDate(now))); + + const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000); + allDepartures.push(...generateDeparturesForDate(tomorrow, getProfileForDate(tomorrow))); + + const nowMs = now.getTime(); + const futureDepartures = allDepartures + .filter(d => d.getTime() >= nowMs) + .sort((a, b) => a.getTime() - b.getTime()); + + const waitMinutes = futureDepartures.slice(0, 3).map(d => { + const diffSec = (d.getTime() - nowMs) / 1000; + return Math.max(1, Math.round(diffSec / 60)); + }); + + return waitMinutes; +} + +/** + * Formate le temps d'attente en minutes pour affichage compatible XXhXX min + */ +export function formatWaitTimeDisplay(waitMinutes) { + if (waitMinutes == null || isNaN(waitMinutes)) { + return { val: '--', unit: 'min' }; + } + const mins = Math.max(1, Math.round(waitMinutes)); + if (mins < 60) { + return { val: String(mins), unit: 'min' }; + } + const h = Math.floor(mins / 60); + const m = mins % 60; + const mStr = m < 10 ? `0${m}` : String(m); + return { val: `${h}h${mStr}`, unit: 'min' }; +} + +/** + * Générateur de lignes contextuelles de secours pour une commune ou gare intermédiaire + */ +function generateContextualLinesForStation(stationName) { + const cleanName = (stationName || 'Gare').replace(/^(Gare\s+de\s+|Gare\s+d\'|Gare\s+)/i, '').trim(); + + return [ + { + id: 'local-bus-1', + type: 'bus', + line: 'L1', + color: BADGE_COLOR_PALETTE[0].bg, + textColor: BADGE_COLOR_PALETTE[0].text, + destinations: [ + { + name: `Gare - (${cleanName})`, + calendars: { + SEMAINE: { firstDep: '05:30', lastDep: '23:30', peakFreq: 8, offpeakFreq: 15, eveningFreq: 25 }, + SAMEDI: { firstDep: '06:30', lastDep: '23:00', peakFreq: 15, offpeakFreq: 20, eveningFreq: 30 }, + DIMANCHE_FERIE: { firstDep: '07:30', lastDep: '22:30', peakFreq: 25, offpeakFreq: 35, eveningFreq: 45 } + } + }, + { + name: 'Gare Routière - (Centre-Ville)', + calendars: { + SEMAINE: { firstDep: '05:45', lastDep: '23:45', peakFreq: 10, offpeakFreq: 18, eveningFreq: 30 }, + SAMEDI: { firstDep: '06:45', lastDep: '23:15', peakFreq: 18, offpeakFreq: 22, eveningFreq: 35 }, + DIMANCHE_FERIE: { firstDep: '07:45', lastDep: '22:45', peakFreq: 30, offpeakFreq: 40, eveningFreq: 50 } + } + } + ] + } + ]; +} + +/** + * Recherche dynamique des correspondances réelles depuis Navitia et APIs de données publiques + * @param {string} stationName - Nom de la gare affichée + * @returns {Promise} Liste des groupes de correspondances avec lignes, destinations et horaires réels + */ +export async function getCorrespondancesForStation(stationName) { + const now = new Date(); + const raw = String(stationName || '').trim(); + const norm = raw.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase(); + + // 1. Résolution des coordonnées GPS de la gare (GTFS) + const gtfsGare = findGtfsGare(raw); + const lat = gtfsGare?.lat; + const lon = gtfsGare?.lon; + + // 2. Recherche prioritaire via l'API NAVITIA / SNCF (rayon strict 200m) + if (lat != null && lon != null) { + try { + const navitiaLines = await fetchTransitFromNavitia(lat, lon, gtfsGare?.nom || raw); + if (navitiaLines && navitiaLines.length > 0) { + return navitiaLines.map(lg => ({ + ...lg, + destinations: lg.destinations.map(d => ({ + ...d, + waitTimes: computeLiveWaitTimes(d, now) + })) + })); + } + } catch (err) { + console.warn('[correspondancesService] API SNCF/Navitia non disponible:', err); + } + + // 3. Recherche via l'API de données publiques Open Transit (rayon strict 200m) + try { + const openApiLines = await fetchTransitFromOpenApi(lat, lon, gtfsGare?.nom || raw); + if (openApiLines && openApiLines.length > 0) { + return openApiLines.map(lg => ({ + ...lg, + destinations: lg.destinations.map(d => ({ + ...d, + waitTimes: computeLiveWaitTimes(d, now) + })) + })); + } + } catch (err) { + console.warn('[correspondancesService] OpenApi non disponible:', err); + } + } + + // 4. Recherche par mot-clé prioritaire dans le référentiel territorial de secours (< 200m) + if (norm.length > 0) { + const keywordMatch = TRANSIT_POLES_FALLBACK.find(pole => + pole.keywords.some(kw => norm.includes(kw)) + ); + + if (keywordMatch) { + return keywordMatch.lines.map(lg => ({ + ...lg, + destinations: lg.destinations.map(d => ({ + ...d, + waitTimes: computeLiveWaitTimes(d, now) + })) + })); + } + } + + // 5. Recherche par proximité géographique GPS + if (lat != null && lon != null) { + let nearestPole = null; + let minDistance = 99999; + + for (const pole of TRANSIT_POLES_FALLBACK) { + const dist = getDistanceKm(lat, lon, pole.center.lat, pole.center.lon); + if (dist <= pole.radiusKm && dist < minDistance) { + minDistance = dist; + nearestPole = pole; + } + } + + if (nearestPole) { + return nearestPole.lines.map(lg => ({ + ...lg, + destinations: lg.destinations.map(d => ({ + ...d, + waitTimes: computeLiveWaitTimes(d, now) + })) + })); + } + } + + // 6. Génération contextuelle de secours adaptée au nom de la gare + const contextualLines = generateContextualLinesForStation(gtfsGare?.nom || raw); + return contextualLines.map(lg => ({ + ...lg, + destinations: lg.destinations.map(d => ({ + ...d, + waitTimes: computeLiveWaitTimes(d, now) + })) + })); +} diff --git a/src/services/gtfsStations.js b/src/services/gtfsStations.js index c52b3ad..c2bc916 100644 --- a/src/services/gtfsStations.js +++ b/src/services/gtfsStations.js @@ -24608,8 +24608,8 @@ const STATION_ALIASES = { 'aix tgv': '87319012', 'aix en provence tgv': '87319012', 'aix centre': '87754002', - 'besancon': '87713008', - 'besancon viotte': '87713008', + 'besancon': '87718007', + 'besancon viotte': '87718007', 'besancon tgv': '87300863', 'besancon franche comte tgv': '87300863', 'dijon': '87713040', diff --git a/src/services/sncfApi.js b/src/services/sncfApi.js index 2d7adb1..8b40de5 100644 --- a/src/services/sncfApi.js +++ b/src/services/sncfApi.js @@ -341,49 +341,65 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { const currentDay = now.getDay(); // 0 = Dimanche, 1 = Lundi, ..., 6 = Samedi const currentYYYYMMDD = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`; - // 1. Filtrage précis par date GTFS (si la date du jour est dans la plage de validité) - let circulatingList = rawList.filter(t => { - // Si la date courante figure dans les dates explicites - if (Array.isArray(t.dates) && t.dates.length > 0 && t.dates.includes(currentYYYYMMDD)) { - return true; + const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000); + const tomorrowDay = tomorrow.getDay(); + const tomorrowYYYYMMDD = `${tomorrow.getFullYear()}${String(tomorrow.getMonth() + 1).padStart(2, '0')}${String(tomorrow.getDate()).padStart(2, '0')}`; + + // Helper de vérification des règles de circulation calendaires + const circulatesOnDate = (t, targetDay, targetYYYYMMDD, targetDate) => { + const pad = (n) => String(n).padStart(2, '0'); + const targetDateHyphen = `${targetDate.getFullYear()}-${pad(targetDate.getMonth() + 1)}-${pad(targetDate.getDate())}`; + + // 1. Base GTFS : si t.dates existe (tableau précis de dates YYYYMMDD), c'est la source de vérité absolue + if (Array.isArray(t.dates) && t.dates.length > 0) { + return t.dates.includes(targetYYYYMMDD) || t.dates.includes(targetDateHyphen); } - // Si le jour de la semaine est actif dans la période de validité - if (Array.isArray(t.days) && t.days.length > 0 && t.days.includes(currentDay)) { - if ((!t.start || currentYYYYMMDD >= t.start) && (!t.end || currentYYYYMMDD <= t.end)) { - return true; + + // 2. Base Supabase : jours personnalisés + const customDaysRaw = t.jours_personnalises; + if (customDaysRaw) { + let customDays = []; + 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); } } - // Train de nuit parti la veille et arrivant aujourd'hui (heures >= 24h) + + // 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; + const df = t.date_fin ? String(t.date_fin).slice(0, 10) : null; + if (db && targetDateHyphen < db) return false; + if (df && targetDateHyphen > df) return false; + return Boolean(t[colName]); + } + + // 4. Jours GTFS (fallback si t.dates n'était pas fourni) if (Array.isArray(t.days) && t.days.length > 0) { - const prevDay = (currentDay + 6) % 7; - const rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr); - const m = String(rawTime).match(/^(\d{1,2})/); - const rawHour = m ? parseInt(m[1], 10) : 0; - if (rawHour >= 24 && t.days.includes(prevDay)) { - return true; - } - } - // Si aucune règle n'est renseignée - if (!t.dates?.length && !t.days?.length) return true; - return false; - }); - - // 2. Fallback robuste : si la date actuelle est hors calendrier GTFS, filtrer par jour de la semaine - if (circulatingList.length === 0) { - circulatingList = rawList.filter(t => { - if (Array.isArray(t.days) && t.days.length > 0) { - return t.days.includes(currentDay); - } + if (!t.days.includes(targetDay)) return false; + if (t.start && targetYYYYMMDD < String(t.start).replace(/-/g, '')) return false; + if (t.end && targetYYYYMMDD > String(t.end).replace(/-/g, '')) return false; return true; - }); - } + } - // 3. Fallback complet : si toujours vide, renvoyer la grille quotidienne complète - if (circulatingList.length === 0) { - circulatingList = rawList; - } + // 5. Date unique fixée (date_depart, date, etc.) + const singleDate = t.date_depart || t.date || t.service_date; + if (singleDate) { + const sStr = String(singleDate).slice(0, 10); + return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetYYYYMMDD; + } - // Helper de résolution ultra-robuste de quai par couple (train, gare) + return true; + }; + + // Helper de résolution de quai const lookupPlatform = (trainNum, tripId, stationUic, stationName) => { const cNum = cleanNum(trainNum); const cName = stationName ? formatStationName(stationName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : ''; @@ -402,19 +418,18 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { ); }; - // Enrichir chaque train circulant avec le GTFS-RT et le SIRI Lite - const enrichedTrains = circulatingList.map((t, idx) => { + // Helper d'enrichissement d'un train + const mapTrain = (t, idx, isTomorrow = false) => { 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 (par trip_id ou numéro de train) - const rt = rtData.updates.get(t.trip_id) || (t.num ? rtData.updates.get(t.num) : null) || (cleanNum(t.num) ? rtData.updates.get(cleanNum(t.num)) : null); - const siri = 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); + // Recherche de mise à jour GTFS-RT ou SIRI-Lite (valide pour aujourd'hui) + 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 isCanceled = (rt && rt.isCanceled) || (siri && siri.isCanceled) || false; const delayMinutes = (rt && rt.delayMinutes > 0) ? rt.delayMinutes : (siri && siri.delayMinutes > 0 ? siri.delayMinutes : 0); - // Extraction du quai/voie réel STRICTEMENT pour cette gare (uic) 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') { @@ -431,13 +446,11 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { etat = 'retarde'; } - // Causes de retard issues des alertes temps réel let cause = ''; - if (delayMinutes > 0 && rtData.alerts.length > 0) { + if (!isTomorrow && delayMinutes > 0 && rtData.alerts.length > 0) { cause = rtData.alerts[idx % rtData.alerts.length].text; } - // Formatage des arrêts intermédiaires avec leurs voies respectives (propres à chaque gare) const formattedStops = (t.stops || []).map((st) => { const stopUic = st.uic || ''; const stopVoie = lookupPlatform(t.num, t.trip_id, stopUic, st.nom); @@ -454,7 +467,7 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { }); return { - id: `gtfs-${t.num || idx}-${rawTime}-${idx}`, + id: `gtfs-${isTomorrow ? 'demain-' : ''}${t.num || idx}-${rawTime}-${idx}`, trip_id: t.trip_id, numero_train: t.num || String(1000 + idx), type_train: t.type || 'TER', @@ -477,6 +490,7 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { stops: formattedStops, composition: (t.type || '').includes('TGV') ? 'Rame 1 (Voitures 1 à 8) + Rame 2 (Voitures 11 à 18)' : 'Train standard', is_substitution: (t.type || '').includes('AUTOCAR'), + _isTomorrow: isTomorrow, _displayTime: formatTimeHM(rawTime), _displayPlatform: realVoie, _depHM: timeHM, @@ -489,16 +503,54 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') { causeDetails: cause || null } }; - }); + }; - // Tri chronologique : afficher les trains circulant à partir de l'heure courante - enrichedTrains.sort((a, b) => { - const aDiff = ((a._depHM || 0) - currentHM + 1440) % 1440; - const bDiff = ((b._depHM || 0) - currentHM + 1440) % 1440; - return aDiff - bDiff; - }); + // 1. Filtrer les trains circulant aujourd'hui + let rawToday = rawList.filter(t => circulatesOnDate(t, currentDay, currentYYYYMMDD, now)); + if (rawToday.length === 0) { + // Fallback si pas de calendrier GTFS explicite + rawToday = rawList.filter(t => Array.isArray(t.days) && t.days.includes(currentDay)); + if (rawToday.length === 0) rawToday = rawList; + } - return enrichedTrains; + const enrichedToday = rawToday.map((t, idx) => mapTrain(t, idx, false)); + + // Filtrer les trains passés : "Une fois que l'heure de départ / Arrivée est dépassée, alors ne plus l'afficher" + const remainingToday = enrichedToday + .filter(t => { + const trainHM = (mode === 'arrivees' ? t._arrHM : t._depHM) || 0; + const delay = t.retard_minutes || 0; + return (trainHM + delay) >= currentHM; + }) + .sort((a, b) => { + const aHM = (mode === 'arrivees' ? a._arrHM : a._depHM) || 0; + const bHM = (mode === 'arrivees' ? b._arrHM : b._depHM) || 0; + return aHM - bHM; + }); + + // 2. Filtrer les trains circulant demain + let rawTomorrow = rawList.filter(t => circulatesOnDate(t, tomorrowDay, tomorrowYYYYMMDD, tomorrow)); + if (rawTomorrow.length === 0) { + rawTomorrow = rawList.filter(t => Array.isArray(t.days) && t.days.includes(tomorrowDay)); + } + + const enrichedTomorrow = rawTomorrow + .map((t, idx) => mapTrain(t, idx, true)) + .sort((a, b) => { + const aHM = (mode === 'arrivees' ? a._arrHM : a._depHM) || 0; + const bHM = (mode === 'arrivees' ? b._arrHM : b._depHM) || 0; + return aHM - bHM; + }); + + // 3. Logique : s'il reste <= 5 horaires aujourd'hui, afficher ceux d'aujourd'hui + ceux du lendemain + let resultList = []; + if (remainingToday.length <= 5) { + resultList = [...remainingToday, ...enrichedTomorrow]; + } else { + resultList = remainingToday; + } + + return resultList; } catch (err) { console.warn('[sncfApi] Erreur de chargement GTFS station:', err); return null;