V1.1.2 //
-> Conversion du temps de retard pour >=60 Min -> Amélioration du rendu EVA (AFFICHEUR ABANDONNÉ PAR SNCF, Abandon possible) -> AFFICHAGE DE TOUTES LES DONNÉES EN MÊME TEMPS
This commit is contained in:
1 parent
23828a54ef
commit
8f00da0896
13 files changed
+1009
-164
No files matched your search
+45
-20
@@ -1,18 +1,6 @@
|
|||||||
const CACHE_NAME = 'ferrovia-pwa-v1';
|
const CACHE_NAME = 'ferrovia-pwa-v2';
|
||||||
|
|
||||||
self.addEventListener('install', (event) => {
|
self.addEventListener('install', (event) => {
|
||||||
event.waitUntil(
|
|
||||||
caches.open(CACHE_NAME).then((cache) => {
|
|
||||||
return cache.addAll([
|
|
||||||
'./',
|
|
||||||
'./index.html',
|
|
||||||
'./manifest.json',
|
|
||||||
'./favicon.ico',
|
|
||||||
'./logo192.png',
|
|
||||||
'./logo512.png'
|
|
||||||
]);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -28,25 +16,62 @@ self.addEventListener('activate', (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('fetch', (event) => {
|
self.addEventListener('fetch', (event) => {
|
||||||
// Ignorer les requêtes non-GET et les requêtes vers d'autres origines ou APIs
|
|
||||||
if (event.request.method !== 'GET' || !event.request.url.startsWith(self.location.origin)) {
|
if (event.request.method !== 'GET' || !event.request.url.startsWith(self.location.origin)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pour la navigation (HTML / pages), stratégie Network-First systématique pour toujours charger la dernière version
|
||||||
|
const isNavigation = event.request.mode === 'navigate' || (event.request.headers.get('accept') && event.request.headers.get('accept').includes('text/html'));
|
||||||
|
|
||||||
|
if (isNavigation) {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request)
|
||||||
|
.then((networkResponse) => {
|
||||||
|
if (networkResponse && networkResponse.ok) {
|
||||||
|
const clone = networkResponse.clone();
|
||||||
|
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
||||||
|
}
|
||||||
|
return networkResponse;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return caches.match(event.request).then((cached) => cached || caches.match('/index.html') || caches.match('./index.html'));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour les scripts JS et CSS, Network-First pour éviter le gel des bundles
|
||||||
|
const isCodeAsset = event.request.url.includes('/static/js/') || event.request.url.includes('/static/css/');
|
||||||
|
if (isCodeAsset) {
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request)
|
||||||
|
.then((networkResponse) => {
|
||||||
|
if (networkResponse && networkResponse.ok) {
|
||||||
|
const clone = networkResponse.clone();
|
||||||
|
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
||||||
|
}
|
||||||
|
return networkResponse;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return caches.match(event.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour les autres ressources (images, favicons, polices), Cache-First avec mise en cache réseau
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches.match(event.request).then((cachedResponse) => {
|
caches.match(event.request).then((cachedResponse) => {
|
||||||
if (cachedResponse) {
|
if (cachedResponse) {
|
||||||
return cachedResponse;
|
return cachedResponse;
|
||||||
}
|
}
|
||||||
return fetch(event.request).then((networkResponse) => {
|
return fetch(event.request).then((networkResponse) => {
|
||||||
return caches.open(CACHE_NAME).then((cache) => {
|
if (networkResponse && networkResponse.ok) {
|
||||||
cache.put(event.request, networkResponse.clone());
|
const clone = networkResponse.clone();
|
||||||
|
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
||||||
|
}
|
||||||
return networkResponse;
|
return networkResponse;
|
||||||
});
|
});
|
||||||
}).catch(() => {
|
|
||||||
// En cas d'échec réseau total, on pourrait retourner un fallback hors-ligne
|
|
||||||
return caches.match('./index.html');
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -21,6 +21,7 @@ if ('serviceWorker' in navigator) {
|
|||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
navigator.serviceWorker.register('/sw.js').then(registration => {
|
navigator.serviceWorker.register('/sw.js').then(registration => {
|
||||||
console.log('SW registered: ', registration);
|
console.log('SW registered: ', registration);
|
||||||
|
registration.update();
|
||||||
}).catch(registrationError => {
|
}).catch(registrationError => {
|
||||||
console.log('SW registration failed: ', registrationError);
|
console.log('SW registration failed: ', registrationError);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,6 +55,215 @@ const BusIcon = ({ size = '59px', color = 'white', style = {} }) => (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Icône Débarquement (Arrivées AFL)
|
||||||
|
const AFLDebarquementIcon = ({ size = '49px', color = 'white', style = {} }) => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
fill="none"
|
||||||
|
style={{ flexShrink: 0, ...style }}
|
||||||
|
>
|
||||||
|
{/* Voiture de train / Porte à gauche */}
|
||||||
|
<path d="M10 4v24M10 4H4M10 28H4" stroke={color} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
<path d="M10 25h3" stroke={color} strokeWidth="2.5" strokeLinecap="round" />
|
||||||
|
<path d="M13 28h3" stroke={color} strokeWidth="2.5" strokeLinecap="round" />
|
||||||
|
{/* Silhouette du passager descendant du train vers le quai (tourné vers la droite) */}
|
||||||
|
<circle cx="20.5" cy="8.5" r="2.5" fill={color} />
|
||||||
|
<path
|
||||||
|
d="M19 13h4l1.5 7-2.5 1-1-4-2 7-3-1 1.5-10z"
|
||||||
|
fill={color}
|
||||||
|
/>
|
||||||
|
<path d="M17 15l-3 3" stroke={color} strokeWidth="2" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pastilles de modification de parcours
|
||||||
|
const AFLRouteBadge = ({ type = 'modified', size = '52px', style = {} }) => {
|
||||||
|
if (type === 'cancelled' || type === 'suppressed') {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style={{ flexShrink: 0, marginRight: '14px', display: 'inline-block', verticalAlign: 'middle', ...style }}
|
||||||
|
>
|
||||||
|
<circle cx="18" cy="18" r="18" fill="#c8102e" />
|
||||||
|
<path d="M11 11L25 25M25 11L11 25" stroke="#ffffff" strokeWidth="4.2" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'added') {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style={{ flexShrink: 0, marginRight: '14px', display: 'inline-block', verticalAlign: 'middle', ...style }}
|
||||||
|
>
|
||||||
|
<circle cx="18" cy="18" r="18" fill="#0084ff" />
|
||||||
|
<path d="M18 9v18M9 18h18" stroke="#ffffff" strokeWidth="4.2" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style={{ flexShrink: 0, marginRight: '14px', display: 'inline-block', verticalAlign: 'middle', ...style }}
|
||||||
|
>
|
||||||
|
<circle cx="18" cy="18" r="18" fill="#38d9f5" />
|
||||||
|
<rect x="15.8" y="8" width="4.4" height="12.5" rx="2.2" fill="#002d62" />
|
||||||
|
<circle cx="18" cy="26" r="2.5" fill="#002d62" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper détection des modifications de parcours
|
||||||
|
const detectRouteModification = (horaire, allPerturbs = [], isDeparts = false, normalizeName = (s) => s) => {
|
||||||
|
if (!horaire) return { destModif: null, perturbStopsMap: new Map() };
|
||||||
|
|
||||||
|
const trainNo = String(horaire.numero_train || horaire.number || horaire.numero || '').trim();
|
||||||
|
const horaireId = String(horaire.id || '').trim();
|
||||||
|
|
||||||
|
const matchingPerturbs = (allPerturbs || []).filter(p => {
|
||||||
|
if (!p) return false;
|
||||||
|
const pRef = String(p.horaire_id || p.horaire || p.horaireId || p.numero_train || p.train_number || p.numero || '').trim();
|
||||||
|
const pNum = String(p.number || p.train_number || p.numero || p.numero_train || '').trim();
|
||||||
|
if (horaireId && pRef === horaireId) return true;
|
||||||
|
if (trainNo && (pRef === trainNo || pNum === trainNo)) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const perturbStopsMap = new Map();
|
||||||
|
let explicitNewDest = null;
|
||||||
|
let explicitOldDest = null;
|
||||||
|
let explicitNewOrigin = null;
|
||||||
|
let explicitOldOrigin = null;
|
||||||
|
|
||||||
|
matchingPerturbs.forEach(p => {
|
||||||
|
if (p.nouveau_terminus || p.nouvelle_destination || p.gare_arrivee_modifiee) {
|
||||||
|
explicitNewDest = p.nouveau_terminus || p.nouvelle_destination || p.gare_arrivee_modifiee;
|
||||||
|
}
|
||||||
|
if (p.ancien_terminus || p.ancienne_destination || p.gare_arrivee_initiale) {
|
||||||
|
explicitOldDest = p.ancien_terminus || p.ancienne_destination || p.gare_arrivee_initiale;
|
||||||
|
}
|
||||||
|
if (p.nouvelle_origine || p.nouvelle_gare_depart || p.gare_depart_modifiee) {
|
||||||
|
explicitNewOrigin = p.nouvelle_origine || p.nouvelle_gare_depart || p.gare_depart_modifiee;
|
||||||
|
}
|
||||||
|
if (p.ancienne_origine || p.ancienne_gare_depart || p.gare_depart_initiale) {
|
||||||
|
explicitOldOrigin = p.ancienne_origine || p.ancienne_gare_depart || p.gare_depart_initiale;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawArrets = p.perturbations_arrets || p.arrets || p.arrets_perturbations;
|
||||||
|
let arretsList = [];
|
||||||
|
if (Array.isArray(rawArrets)) {
|
||||||
|
arretsList = rawArrets;
|
||||||
|
} else if (rawArrets && typeof rawArrets === 'object' && Array.isArray(rawArrets.arrets)) {
|
||||||
|
arretsList = rawArrets.arrets;
|
||||||
|
} else if (typeof rawArrets === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawArrets);
|
||||||
|
arretsList = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.arrets) ? parsed.arrets : []);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
arretsList.forEach(a => {
|
||||||
|
const aName = a.gare_nom || a.nom || a.name || a.station_name || (typeof a === 'string' ? a : '');
|
||||||
|
if (!aName) return;
|
||||||
|
const aType = String(a.type_perturbation || a.type || a.status || '').toLowerCase();
|
||||||
|
const norm = normalizeName(aName);
|
||||||
|
perturbStopsMap.set(norm, {
|
||||||
|
name: aName,
|
||||||
|
type: aType,
|
||||||
|
isSupprime: aType.includes('suppr') || aType.includes('annul') || aType.includes('canc'),
|
||||||
|
isAjoute: aType.includes('ajout') || aType.includes('add') || aType.includes('nouv'),
|
||||||
|
isModifie: aType.includes('modif')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (horaire.destination_initiale || horaire.gare_arrivee_initiale || horaire.ancienne_destination) {
|
||||||
|
explicitOldDest = horaire.destination_initiale || horaire.gare_arrivee_initiale || horaire.ancienne_destination;
|
||||||
|
}
|
||||||
|
if (horaire.nouvelle_destination || horaire.nouveau_terminus || (horaire.destination_modifiee && horaire.gare_arrivee_nom)) {
|
||||||
|
explicitNewDest = horaire.nouvelle_destination || horaire.nouveau_terminus || horaire.gare_arrivee_nom;
|
||||||
|
}
|
||||||
|
if (horaire.origine_initiale || horaire.gare_depart_initiale || horaire.ancienne_origine) {
|
||||||
|
explicitOldOrigin = horaire.origine_initiale || horaire.gare_depart_initiale || horaire.ancienne_origine;
|
||||||
|
}
|
||||||
|
if (horaire.nouvelle_origine || (horaire.origine_modifiee && horaire.gare_depart_nom)) {
|
||||||
|
explicitNewOrigin = horaire.nouvelle_origine || horaire.gare_depart_nom;
|
||||||
|
}
|
||||||
|
|
||||||
|
let destModif = null;
|
||||||
|
|
||||||
|
if (isDeparts) {
|
||||||
|
const currentDest = horaire.gare_arrivee_nom || horaire.destination || '';
|
||||||
|
const normDest = normalizeName(currentDest);
|
||||||
|
const destInPerturb = perturbStopsMap.get(normDest);
|
||||||
|
|
||||||
|
if (explicitNewDest && explicitOldDest && normalizeName(explicitNewDest) !== normalizeName(explicitOldDest)) {
|
||||||
|
destModif = {
|
||||||
|
newStation: explicitNewDest,
|
||||||
|
oldStation: explicitOldDest,
|
||||||
|
isAdded: Boolean(destInPerturb?.isAjoute || /ajout/.test(String(horaire.type_modification || '')))
|
||||||
|
};
|
||||||
|
} else if (destInPerturb?.isSupprime) {
|
||||||
|
destModif = {
|
||||||
|
newStation: explicitNewDest || 'Terminus modifié',
|
||||||
|
oldStation: currentDest,
|
||||||
|
isAdded: false
|
||||||
|
};
|
||||||
|
} else if (destInPerturb?.isAjoute) {
|
||||||
|
destModif = {
|
||||||
|
newStation: currentDest,
|
||||||
|
oldStation: explicitOldDest || 'Destination modifiée',
|
||||||
|
isAdded: true
|
||||||
|
};
|
||||||
|
} else if (explicitOldDest && normalizeName(explicitOldDest) !== normDest) {
|
||||||
|
destModif = {
|
||||||
|
newStation: currentDest,
|
||||||
|
oldStation: explicitOldDest,
|
||||||
|
isAdded: Boolean(horaire.destination_ajoutee)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const currentOrig = horaire.gare_depart_nom || horaire.origin || '';
|
||||||
|
const normOrig = normalizeName(currentOrig);
|
||||||
|
const origInPerturb = perturbStopsMap.get(normOrig);
|
||||||
|
|
||||||
|
if (explicitNewOrigin && explicitOldOrigin && normalizeName(explicitNewOrigin) !== normalizeName(explicitOldOrigin)) {
|
||||||
|
destModif = {
|
||||||
|
newStation: explicitNewOrigin,
|
||||||
|
oldStation: explicitOldOrigin,
|
||||||
|
isAdded: Boolean(origInPerturb?.isAjoute)
|
||||||
|
};
|
||||||
|
} else if (origInPerturb?.isSupprime) {
|
||||||
|
destModif = {
|
||||||
|
newStation: explicitNewOrigin || 'Origine modifiée',
|
||||||
|
oldStation: currentOrig,
|
||||||
|
isAdded: false
|
||||||
|
};
|
||||||
|
} else if (origInPerturb?.isAjoute) {
|
||||||
|
destModif = {
|
||||||
|
newStation: currentOrig,
|
||||||
|
oldStation: explicitOldOrigin || 'Origine initiale',
|
||||||
|
isAdded: true
|
||||||
|
};
|
||||||
|
} else if (explicitOldOrigin && normalizeName(explicitOldOrigin) !== normOrig) {
|
||||||
|
destModif = {
|
||||||
|
newStation: currentOrig,
|
||||||
|
oldStation: explicitOldOrigin,
|
||||||
|
isAdded: Boolean(horaire.origine_ajoutee)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { destModif, perturbStopsMap };
|
||||||
|
};
|
||||||
|
|
||||||
const ViasMarquee = ({ viaNames }) => {
|
const ViasMarquee = ({ viaNames }) => {
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const textRef = useRef(null);
|
const textRef = useRef(null);
|
||||||
@@ -68,7 +277,6 @@ const ViasMarquee = ({ viaNames }) => {
|
|||||||
const tWidth = textRef.current.scrollWidth;
|
const tWidth = textRef.current.scrollWidth;
|
||||||
if (tWidth > cWidth) {
|
if (tWidth > cWidth) {
|
||||||
setIsOverflowing(true);
|
setIsOverflowing(true);
|
||||||
// Vitesse de défilement rapide (~280px/s)
|
|
||||||
setDuration(Math.max(1.0, tWidth / 280));
|
setDuration(Math.max(1.0, tWidth / 280));
|
||||||
} else {
|
} else {
|
||||||
setIsOverflowing(false);
|
setIsOverflowing(false);
|
||||||
@@ -99,18 +307,38 @@ const ViasMarquee = ({ viaNames }) => {
|
|||||||
animationDuration: isOverflowing ? `${duration}s` : undefined
|
animationDuration: isOverflowing ? `${duration}s` : undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{viaNames.map((v, i) => (
|
{viaNames.map((item, i) => {
|
||||||
|
const name = typeof item === 'string' ? item : (item.name || item.gare_nom || '');
|
||||||
|
const isSupprime = typeof item === 'object' && Boolean(item.isSupprime);
|
||||||
|
const isAjoute = typeof item === 'object' && Boolean(item.isAjoute);
|
||||||
|
const isModifie = typeof item === 'object' && Boolean(item.isModifie);
|
||||||
|
|
||||||
|
return (
|
||||||
<React.Fragment key={i}>
|
<React.Fragment key={i}>
|
||||||
{i > 0 && <span className="afl-arr-vias-chevron">></span>}
|
{i > 0 && <span className="afl-arr-vias-chevron">></span>}
|
||||||
<span className="afl-arr-vias-stop">{v}</span>
|
<span
|
||||||
|
className="afl-arr-vias-stop"
|
||||||
|
style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
...(isSupprime ? { textDecoration: 'line-through', opacity: 0.85 } : {})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSupprime && <AFLRouteBadge type="cancelled" size="26px" style={{ marginRight: '6px' }} />}
|
||||||
|
{isAjoute && <AFLRouteBadge type="added" size="26px" style={{ marginRight: '6px' }} />}
|
||||||
|
{isModifie && <AFLRouteBadge type="modified" size="26px" style={{ marginRight: '6px' }} />}
|
||||||
|
<span>{name}</span>
|
||||||
|
</span>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function Clock(){
|
function Clock(){
|
||||||
const [now, setNow] = useState(new Date());
|
const [now, setNow] = useState(new Date());
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -629,7 +857,12 @@ export default function AFLArrivees(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null };
|
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null };
|
||||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay };
|
if (maxDelay != null) {
|
||||||
|
const dH = Math.floor(maxDelay / 60);
|
||||||
|
const dRem = maxDelay % 60;
|
||||||
|
const dStr = maxDelay >= 60 ? (dRem > 0 ? `${dH}h${String(dRem).padStart(2, '0')}` : `${dH}h`) : `${maxDelay}min.`;
|
||||||
|
return { label: `retardé +${dStr}`, detail: '', delayMinutes: maxDelay };
|
||||||
|
}
|
||||||
return { label: "à l'heure", detail: '', delayMinutes: null };
|
return { label: "à l'heure", detail: '', delayMinutes: null };
|
||||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||||
|
|
||||||
@@ -1137,7 +1370,7 @@ export default function AFLArrivees(){
|
|||||||
const intervalId = setInterval(fetchData, intervalMs);
|
const intervalId = setInterval(fetchData, intervalMs);
|
||||||
|
|
||||||
return () => { mounted = false; clearInterval(intervalId); };
|
return () => { mounted = false; clearInterval(intervalId); };
|
||||||
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]);
|
}, [gareDisplay, gareParam, gareIdParam]);
|
||||||
|
|
||||||
// Helpers pour perturbations (memoized plus haut)
|
// Helpers pour perturbations (memoized plus haut)
|
||||||
|
|
||||||
@@ -1249,16 +1482,7 @@ export default function AFLArrivees(){
|
|||||||
{/* HEADER */}
|
{/* HEADER */}
|
||||||
<header className="afl-arr-header">
|
<header className="afl-arr-header">
|
||||||
<div className="afl-arr-header-left">
|
<div className="afl-arr-header-left">
|
||||||
<svg width="49px" height="49px" viewBox="0 0 28 28" fill="none" style={{ flexShrink: 0 }}>
|
<AFLDebarquementIcon size="49px" color="white" />
|
||||||
{/* Porte de train verticale */}
|
|
||||||
<path d="M5 4v20M5 4h3M5 24h3" stroke="white" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
{/* Silhouette tête */}
|
|
||||||
<circle cx="10.5" cy="7" r="2.2" fill="white" />
|
|
||||||
{/* Silhouette corps + marche vers la droite */}
|
|
||||||
<path d="M8.5 11h4l1.5 5.5-2 1-1-3.5-1 5.5-3-1 2-7.5z" fill="white" />
|
|
||||||
{/* Chevrons >> */}
|
|
||||||
<path d="M20 8l4 6-4 6M15 8l4 6-4 6" stroke="white" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
<div className="afl-arr-header-title">Arrivées</div>
|
<div className="afl-arr-header-title">Arrivées</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1326,20 +1550,34 @@ export default function AFLArrivees(){
|
|||||||
const pillClass = isSuppressed ? 'afl-arr-pill--suppressed' : isDelayed ? 'afl-arr-pill--delayed' : 'afl-arr-pill--ontime';
|
const pillClass = isSuppressed ? 'afl-arr-pill--suppressed' : isDelayed ? 'afl-arr-pill--delayed' : 'afl-arr-pill--ontime';
|
||||||
|
|
||||||
let delayVal = '+ 5 min';
|
let delayVal = '+ 5 min';
|
||||||
|
const formatDelayVal = (m) => {
|
||||||
|
if (m >= 60) {
|
||||||
|
const h = Math.floor(m / 60);
|
||||||
|
const rem = m % 60;
|
||||||
|
return rem > 0 ? `+ ${h}h${String(rem).padStart(2, '0')}` : `+ ${h}h`;
|
||||||
|
}
|
||||||
|
return `+ ${m} min`;
|
||||||
|
};
|
||||||
if (typeof status?.delayMinutes === 'number' && status.delayMinutes > 0) {
|
if (typeof status?.delayMinutes === 'number' && status.delayMinutes > 0) {
|
||||||
delayVal = `+ ${status.delayMinutes} min`;
|
delayVal = formatDelayVal(status.delayMinutes);
|
||||||
} else if (status?.delayMinutes) {
|
} else if (status?.delayMinutes) {
|
||||||
delayVal = `+ ${status.delayMinutes} min`;
|
const parsed = parseInt(status.delayMinutes, 10);
|
||||||
|
delayVal = !isNaN(parsed) && parsed > 0 ? formatDelayVal(parsed) : `+ ${status.delayMinutes} min`;
|
||||||
} else if (status?.label && status.label.includes('+')) {
|
} else if (status?.label && status.label.includes('+')) {
|
||||||
delayVal = status.label;
|
const m = status.label.match(/\+(\d+)\s*(?:min)?/);
|
||||||
|
if (m) delayVal = formatDelayVal(parseInt(m[1], 10));
|
||||||
|
else delayVal = status.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
let delayDisplayText = alternateDelay ? 'retardé' : delayVal;
|
let delayDisplayText = alternateDelay ? 'retardé' : delayVal;
|
||||||
let pillText = isSuppressed ? 'Supprimé' : isDelayed ? delayDisplayText : 'À l\'heure';
|
let pillText = isSuppressed ? 'Supprimé' : isDelayed ? delayDisplayText : 'À l\'heure';
|
||||||
|
|
||||||
// Calcul des vias (gares entre l'origine et la gare d'arrivée)
|
// Détection des modifications de parcours (Origine modifiée)
|
||||||
|
const { destModif: originModif, perturbStopsMap } = detectRouteModification(horaire, perturbations, false, normalizeName);
|
||||||
|
|
||||||
|
// Calcul des gares de provenance (gares desservies avant la gare affichée dans l'ordre décroissant)
|
||||||
const stopsList = getStops(horaire);
|
const stopsList = getStops(horaire);
|
||||||
const viaNames = [];
|
const rawPrevStops = [];
|
||||||
if (Array.isArray(stopsList)) {
|
if (Array.isArray(stopsList)) {
|
||||||
for (const s of stopsList) {
|
for (const s of stopsList) {
|
||||||
const sName = s.gare_nom || s.nom || s.name || s.station_name || '';
|
const sName = s.gare_nom || s.nom || s.name || s.station_name || '';
|
||||||
@@ -1347,9 +1585,26 @@ export default function AFLArrivees(){
|
|||||||
const norm = normalizeName(sName);
|
const norm = normalizeName(sName);
|
||||||
if (norm === normalizeName(origin || '')) continue;
|
if (norm === normalizeName(origin || '')) continue;
|
||||||
if (norm === normalizeName(stationName || '')) break;
|
if (norm === normalizeName(stationName || '')) break;
|
||||||
viaNames.push(sName);
|
rawPrevStops.push(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Ordre décroissant : la gare la plus proche avant l'arrivée en premier
|
||||||
|
rawPrevStops.reverse();
|
||||||
|
|
||||||
|
const viaItems = rawPrevStops.map(s => {
|
||||||
|
const sName = s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '');
|
||||||
|
const norm = normalizeName(sName);
|
||||||
|
const pInfo = perturbStopsMap.get(norm);
|
||||||
|
const isSupprime = Boolean(pInfo?.isSupprime || s.is_supprime || s.supprime || s.type === 'arret_supprime');
|
||||||
|
const isAjoute = Boolean(pInfo?.isAjoute || s.is_ajoute || s.ajoute || s.type === 'arret_ajoute');
|
||||||
|
const isModifie = Boolean(pInfo?.isModifie || s.is_modifie || s.modifie);
|
||||||
|
return {
|
||||||
|
name: sName,
|
||||||
|
isSupprime,
|
||||||
|
isAjoute,
|
||||||
|
isModifie
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const renderPillIcon = () => {
|
const renderPillIcon = () => {
|
||||||
if (isSuppressed) {
|
if (isSuppressed) {
|
||||||
@@ -1432,16 +1687,44 @@ export default function AFLArrivees(){
|
|||||||
<>
|
<>
|
||||||
<div className="afl-arr-top-row">
|
<div className="afl-arr-top-row">
|
||||||
{isCarTER && <BusIcon size="52px" color="white" style={{ marginRight: '15px' }} />}
|
{isCarTER && <BusIcon size="52px" color="white" style={{ marginRight: '15px' }} />}
|
||||||
|
{originModif ? (
|
||||||
|
alternateDelay ? (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||||
|
<AFLRouteBadge type={originModif.isAdded ? 'added' : 'modified'} size="52px" />
|
||||||
|
<MarqueeText text={originModif.newStation} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||||
|
<AFLRouteBadge type="cancelled" size="52px" />
|
||||||
|
<MarqueeText text={originModif.oldStation} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
<MarqueeText text={origin} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
<MarqueeText text={origin} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="afl-arr-bottom-row">
|
<div className="afl-arr-bottom-row">
|
||||||
<ViasMarquee viaNames={viaNames} />
|
<ViasMarquee viaNames={viaItems} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||||
{isCarTER && <BusIcon size="49px" color="white" style={{ marginRight: '15px' }} />}
|
{isCarTER && <BusIcon size="49px" color="white" style={{ marginRight: '15px' }} />}
|
||||||
|
{originModif ? (
|
||||||
|
alternateDelay ? (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||||
|
<AFLRouteBadge type={originModif.isAdded ? 'added' : 'modified'} size="46px" />
|
||||||
|
<MarqueeText text={originModif.newStation} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||||
|
<AFLRouteBadge type="cancelled" size="46px" />
|
||||||
|
<MarqueeText text={originModif.oldStation} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
<MarqueeText text={origin} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
<MarqueeText text={origin} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,6 +55,215 @@ const BusIcon = ({ size = '59px', color = 'white', style = {} }) => (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Icône Embarquement (Départs AFL)
|
||||||
|
const AFLEmbarquementIcon = ({ size = '49px', color = 'white', style = {} }) => (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
fill="none"
|
||||||
|
style={{ flexShrink: 0, ...style }}
|
||||||
|
>
|
||||||
|
{/* Voiture de train / Porte à droite */}
|
||||||
|
<path d="M22 4v24M22 4h6M22 28h6" stroke={color} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
<path d="M19 25h3" stroke={color} strokeWidth="2.5" strokeLinecap="round" />
|
||||||
|
<path d="M16 28h3" stroke={color} strokeWidth="2.5" strokeLinecap="round" />
|
||||||
|
{/* Silhouette du passager montant dans le train (tourné vers la droite) */}
|
||||||
|
<circle cx="11.5" cy="8.5" r="2.5" fill={color} />
|
||||||
|
<path
|
||||||
|
d="M10 13h4l2 6-2.5 1-1-3.5-1.5 5.5-3-1 2-8z"
|
||||||
|
fill={color}
|
||||||
|
/>
|
||||||
|
<path d="M14 15l4-1.5" stroke={color} strokeWidth="2" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pastilles de modification de parcours
|
||||||
|
const AFLRouteBadge = ({ type = 'modified', size = '52px', style = {} }) => {
|
||||||
|
if (type === 'cancelled' || type === 'suppressed') {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style={{ flexShrink: 0, marginRight: '14px', display: 'inline-block', verticalAlign: 'middle', ...style }}
|
||||||
|
>
|
||||||
|
<circle cx="18" cy="18" r="18" fill="#c8102e" />
|
||||||
|
<path d="M11 11L25 25M25 11L11 25" stroke="#ffffff" strokeWidth="4.2" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (type === 'added') {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style={{ flexShrink: 0, marginRight: '14px', display: 'inline-block', verticalAlign: 'middle', ...style }}
|
||||||
|
>
|
||||||
|
<circle cx="18" cy="18" r="18" fill="#0084ff" />
|
||||||
|
<path d="M18 9v18M9 18h18" stroke="#ffffff" strokeWidth="4.2" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style={{ flexShrink: 0, marginRight: '14px', display: 'inline-block', verticalAlign: 'middle', ...style }}
|
||||||
|
>
|
||||||
|
<circle cx="18" cy="18" r="18" fill="#38d9f5" />
|
||||||
|
<rect x="15.8" y="8" width="4.4" height="12.5" rx="2.2" fill="#002d62" />
|
||||||
|
<circle cx="18" cy="26" r="2.5" fill="#002d62" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper détection des modifications de parcours
|
||||||
|
const detectRouteModification = (horaire, allPerturbs = [], isDeparts = true, normalizeName = (s) => s) => {
|
||||||
|
if (!horaire) return { destModif: null, perturbStopsMap: new Map() };
|
||||||
|
|
||||||
|
const trainNo = String(horaire.numero_train || horaire.number || horaire.numero || '').trim();
|
||||||
|
const horaireId = String(horaire.id || '').trim();
|
||||||
|
|
||||||
|
const matchingPerturbs = (allPerturbs || []).filter(p => {
|
||||||
|
if (!p) return false;
|
||||||
|
const pRef = String(p.horaire_id || p.horaire || p.horaireId || p.numero_train || p.train_number || p.numero || '').trim();
|
||||||
|
const pNum = String(p.number || p.train_number || p.numero || p.numero_train || '').trim();
|
||||||
|
if (horaireId && pRef === horaireId) return true;
|
||||||
|
if (trainNo && (pRef === trainNo || pNum === trainNo)) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const perturbStopsMap = new Map();
|
||||||
|
let explicitNewDest = null;
|
||||||
|
let explicitOldDest = null;
|
||||||
|
let explicitNewOrigin = null;
|
||||||
|
let explicitOldOrigin = null;
|
||||||
|
|
||||||
|
matchingPerturbs.forEach(p => {
|
||||||
|
if (p.nouveau_terminus || p.nouvelle_destination || p.gare_arrivee_modifiee) {
|
||||||
|
explicitNewDest = p.nouveau_terminus || p.nouvelle_destination || p.gare_arrivee_modifiee;
|
||||||
|
}
|
||||||
|
if (p.ancien_terminus || p.ancienne_destination || p.gare_arrivee_initiale) {
|
||||||
|
explicitOldDest = p.ancien_terminus || p.ancienne_destination || p.gare_arrivee_initiale;
|
||||||
|
}
|
||||||
|
if (p.nouvelle_origine || p.nouvelle_gare_depart || p.gare_depart_modifiee) {
|
||||||
|
explicitNewOrigin = p.nouvelle_origine || p.nouvelle_gare_depart || p.gare_depart_modifiee;
|
||||||
|
}
|
||||||
|
if (p.ancienne_origine || p.ancienne_gare_depart || p.gare_depart_initiale) {
|
||||||
|
explicitOldOrigin = p.ancienne_origine || p.ancienne_gare_depart || p.gare_depart_initiale;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawArrets = p.perturbations_arrets || p.arrets || p.arrets_perturbations;
|
||||||
|
let arretsList = [];
|
||||||
|
if (Array.isArray(rawArrets)) {
|
||||||
|
arretsList = rawArrets;
|
||||||
|
} else if (rawArrets && typeof rawArrets === 'object' && Array.isArray(rawArrets.arrets)) {
|
||||||
|
arretsList = rawArrets.arrets;
|
||||||
|
} else if (typeof rawArrets === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawArrets);
|
||||||
|
arretsList = Array.isArray(parsed) ? parsed : (Array.isArray(parsed?.arrets) ? parsed.arrets : []);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
arretsList.forEach(a => {
|
||||||
|
const aName = a.gare_nom || a.nom || a.name || a.station_name || (typeof a === 'string' ? a : '');
|
||||||
|
if (!aName) return;
|
||||||
|
const aType = String(a.type_perturbation || a.type || a.status || '').toLowerCase();
|
||||||
|
const norm = normalizeName(aName);
|
||||||
|
perturbStopsMap.set(norm, {
|
||||||
|
name: aName,
|
||||||
|
type: aType,
|
||||||
|
isSupprime: aType.includes('suppr') || aType.includes('annul') || aType.includes('canc'),
|
||||||
|
isAjoute: aType.includes('ajout') || aType.includes('add') || aType.includes('nouv'),
|
||||||
|
isModifie: aType.includes('modif')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (horaire.destination_initiale || horaire.gare_arrivee_initiale || horaire.ancienne_destination) {
|
||||||
|
explicitOldDest = horaire.destination_initiale || horaire.gare_arrivee_initiale || horaire.ancienne_destination;
|
||||||
|
}
|
||||||
|
if (horaire.nouvelle_destination || horaire.nouveau_terminus || (horaire.destination_modifiee && horaire.gare_arrivee_nom)) {
|
||||||
|
explicitNewDest = horaire.nouvelle_destination || horaire.nouveau_terminus || horaire.gare_arrivee_nom;
|
||||||
|
}
|
||||||
|
if (horaire.origine_initiale || horaire.gare_depart_initiale || horaire.ancienne_origine) {
|
||||||
|
explicitOldOrigin = horaire.origine_initiale || horaire.gare_depart_initiale || horaire.ancienne_origine;
|
||||||
|
}
|
||||||
|
if (horaire.nouvelle_origine || (horaire.origine_modifiee && horaire.gare_depart_nom)) {
|
||||||
|
explicitNewOrigin = horaire.nouvelle_origine || horaire.gare_depart_nom;
|
||||||
|
}
|
||||||
|
|
||||||
|
let destModif = null;
|
||||||
|
|
||||||
|
if (isDeparts) {
|
||||||
|
const currentDest = horaire.gare_arrivee_nom || horaire.destination || '';
|
||||||
|
const normDest = normalizeName(currentDest);
|
||||||
|
const destInPerturb = perturbStopsMap.get(normDest);
|
||||||
|
|
||||||
|
if (explicitNewDest && explicitOldDest && normalizeName(explicitNewDest) !== normalizeName(explicitOldDest)) {
|
||||||
|
destModif = {
|
||||||
|
newStation: explicitNewDest,
|
||||||
|
oldStation: explicitOldDest,
|
||||||
|
isAdded: Boolean(destInPerturb?.isAjoute || /ajout/.test(String(horaire.type_modification || '')))
|
||||||
|
};
|
||||||
|
} else if (destInPerturb?.isSupprime) {
|
||||||
|
destModif = {
|
||||||
|
newStation: explicitNewDest || 'Terminus modifié',
|
||||||
|
oldStation: currentDest,
|
||||||
|
isAdded: false
|
||||||
|
};
|
||||||
|
} else if (destInPerturb?.isAjoute) {
|
||||||
|
destModif = {
|
||||||
|
newStation: currentDest,
|
||||||
|
oldStation: explicitOldDest || 'Destination modifiée',
|
||||||
|
isAdded: true
|
||||||
|
};
|
||||||
|
} else if (explicitOldDest && normalizeName(explicitOldDest) !== normDest) {
|
||||||
|
destModif = {
|
||||||
|
newStation: currentDest,
|
||||||
|
oldStation: explicitOldDest,
|
||||||
|
isAdded: Boolean(horaire.destination_ajoutee)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const currentOrig = horaire.gare_depart_nom || horaire.origin || '';
|
||||||
|
const normOrig = normalizeName(currentOrig);
|
||||||
|
const origInPerturb = perturbStopsMap.get(normOrig);
|
||||||
|
|
||||||
|
if (explicitNewOrigin && explicitOldOrigin && normalizeName(explicitNewOrigin) !== normalizeName(explicitOldOrigin)) {
|
||||||
|
destModif = {
|
||||||
|
newStation: explicitNewOrigin,
|
||||||
|
oldStation: explicitOldOrigin,
|
||||||
|
isAdded: Boolean(origInPerturb?.isAjoute)
|
||||||
|
};
|
||||||
|
} else if (origInPerturb?.isSupprime) {
|
||||||
|
destModif = {
|
||||||
|
newStation: explicitNewOrigin || 'Origine modifiée',
|
||||||
|
oldStation: currentOrig,
|
||||||
|
isAdded: false
|
||||||
|
};
|
||||||
|
} else if (origInPerturb?.isAjoute) {
|
||||||
|
destModif = {
|
||||||
|
newStation: currentOrig,
|
||||||
|
oldStation: explicitOldOrigin || 'Origine initiale',
|
||||||
|
isAdded: true
|
||||||
|
};
|
||||||
|
} else if (explicitOldOrigin && normalizeName(explicitOldOrigin) !== normOrig) {
|
||||||
|
destModif = {
|
||||||
|
newStation: currentOrig,
|
||||||
|
oldStation: explicitOldOrigin,
|
||||||
|
isAdded: Boolean(horaire.origine_ajoutee)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { destModif, perturbStopsMap };
|
||||||
|
};
|
||||||
|
|
||||||
const ViasMarquee = ({ viaNames }) => {
|
const ViasMarquee = ({ viaNames }) => {
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const textRef = useRef(null);
|
const textRef = useRef(null);
|
||||||
@@ -68,7 +277,6 @@ const ViasMarquee = ({ viaNames }) => {
|
|||||||
const tWidth = textRef.current.scrollWidth;
|
const tWidth = textRef.current.scrollWidth;
|
||||||
if (tWidth > cWidth) {
|
if (tWidth > cWidth) {
|
||||||
setIsOverflowing(true);
|
setIsOverflowing(true);
|
||||||
// Vitesse de défilement multipliée par 2 (~280px/s)
|
|
||||||
setDuration(Math.max(1.0, tWidth / 280));
|
setDuration(Math.max(1.0, tWidth / 280));
|
||||||
} else {
|
} else {
|
||||||
setIsOverflowing(false);
|
setIsOverflowing(false);
|
||||||
@@ -99,18 +307,38 @@ const ViasMarquee = ({ viaNames }) => {
|
|||||||
animationDuration: isOverflowing ? `${duration}s` : undefined
|
animationDuration: isOverflowing ? `${duration}s` : undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{viaNames.map((v, i) => (
|
{viaNames.map((item, i) => {
|
||||||
|
const name = typeof item === 'string' ? item : (item.name || item.gare_nom || '');
|
||||||
|
const isSupprime = typeof item === 'object' && Boolean(item.isSupprime);
|
||||||
|
const isAjoute = typeof item === 'object' && Boolean(item.isAjoute);
|
||||||
|
const isModifie = typeof item === 'object' && Boolean(item.isModifie);
|
||||||
|
|
||||||
|
return (
|
||||||
<React.Fragment key={i}>
|
<React.Fragment key={i}>
|
||||||
{i > 0 && <span className="afl-vias-chevron">></span>}
|
{i > 0 && <span className="afl-vias-chevron">></span>}
|
||||||
<span className="afl-vias-stop">{v}</span>
|
<span
|
||||||
|
className="afl-vias-stop"
|
||||||
|
style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
...(isSupprime ? { textDecoration: 'line-through', opacity: 0.85 } : {})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSupprime && <AFLRouteBadge type="cancelled" size="26px" style={{ marginRight: '6px' }} />}
|
||||||
|
{isAjoute && <AFLRouteBadge type="added" size="26px" style={{ marginRight: '6px' }} />}
|
||||||
|
{isModifie && <AFLRouteBadge type="modified" size="26px" style={{ marginRight: '6px' }} />}
|
||||||
|
<span>{name}</span>
|
||||||
|
</span>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// (La fonction TrainType a été retirée car la colonne affiche désormais directement les
|
// (La fonction TrainType a été retirée car la colonne affiche désormais directement les
|
||||||
// informations via .train-meta dans la colonne .col-train-type)
|
// informations via .train-meta dans la colonne .col-train-type)
|
||||||
|
|
||||||
@@ -637,7 +865,12 @@ export default function AFLDeparts(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null };
|
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null };
|
||||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay };
|
if (maxDelay != null) {
|
||||||
|
const dH = Math.floor(maxDelay / 60);
|
||||||
|
const dRem = maxDelay % 60;
|
||||||
|
const dStr = maxDelay >= 60 ? (dRem > 0 ? `${dH}h${String(dRem).padStart(2, '0')}` : `${dH}h`) : `${maxDelay}min.`;
|
||||||
|
return { label: `retardé +${dStr}`, detail: '', delayMinutes: maxDelay };
|
||||||
|
}
|
||||||
return { label: "à l'heure", detail: '', delayMinutes: null };
|
return { label: "à l'heure", detail: '', delayMinutes: null };
|
||||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||||
|
|
||||||
@@ -1164,7 +1397,7 @@ export default function AFLDeparts(){
|
|||||||
const intervalId = setInterval(fetchData, intervalMs);
|
const intervalId = setInterval(fetchData, intervalMs);
|
||||||
|
|
||||||
return () => { mounted = false; clearInterval(intervalId); };
|
return () => { mounted = false; clearInterval(intervalId); };
|
||||||
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours]);
|
}, [gareDisplay, gareParam, gareIdParam]);
|
||||||
|
|
||||||
// Helpers pour perturbations (memoized plus haut)
|
// Helpers pour perturbations (memoized plus haut)
|
||||||
|
|
||||||
@@ -1276,16 +1509,7 @@ export default function AFLDeparts(){
|
|||||||
{/* HEADER */}
|
{/* HEADER */}
|
||||||
<header className="afl-header">
|
<header className="afl-header">
|
||||||
<div className="afl-header-left">
|
<div className="afl-header-left">
|
||||||
<svg width="49px" height="49px" viewBox="0 0 28 28" fill="none" style={{ flexShrink: 0 }}>
|
<AFLEmbarquementIcon size="49px" color="white" />
|
||||||
{/* Chevrons >> */}
|
|
||||||
<path d="M3 8l4 6-4 6M8 8l4 6-4 6" stroke="white" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
{/* Silhouette tête */}
|
|
||||||
<circle cx="17.5" cy="7" r="2.2" fill="white" />
|
|
||||||
{/* Silhouette corps + marche */}
|
|
||||||
<path d="M15.5 11h4l1.5 5.5-2 1-1-3.5-1 5.5-3-1 2-7.5z" fill="white" />
|
|
||||||
{/* Porte de train verticale */}
|
|
||||||
<path d="M23 4v20M23 4h3M23 24h3" stroke="white" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
<div className="afl-header-title">Départs</div>
|
<div className="afl-header-title">Départs</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1353,20 +1577,34 @@ export default function AFLDeparts(){
|
|||||||
const pillClass = isSuppressed ? 'afl-pill--suppressed' : isDelayed ? 'afl-pill--delayed' : 'afl-pill--ontime';
|
const pillClass = isSuppressed ? 'afl-pill--suppressed' : isDelayed ? 'afl-pill--delayed' : 'afl-pill--ontime';
|
||||||
|
|
||||||
let delayVal = '+ 5 min';
|
let delayVal = '+ 5 min';
|
||||||
|
const formatDelayVal = (m) => {
|
||||||
|
if (m >= 60) {
|
||||||
|
const h = Math.floor(m / 60);
|
||||||
|
const rem = m % 60;
|
||||||
|
return rem > 0 ? `+ ${h}h${String(rem).padStart(2, '0')}` : `+ ${h}h`;
|
||||||
|
}
|
||||||
|
return `+ ${m} min`;
|
||||||
|
};
|
||||||
if (typeof status?.delayMinutes === 'number' && status.delayMinutes > 0) {
|
if (typeof status?.delayMinutes === 'number' && status.delayMinutes > 0) {
|
||||||
delayVal = `+ ${status.delayMinutes} min`;
|
delayVal = formatDelayVal(status.delayMinutes);
|
||||||
} else if (status?.delayMinutes) {
|
} else if (status?.delayMinutes) {
|
||||||
delayVal = `+ ${status.delayMinutes} min`;
|
const parsed = parseInt(status.delayMinutes, 10);
|
||||||
|
delayVal = !isNaN(parsed) && parsed > 0 ? formatDelayVal(parsed) : `+ ${status.delayMinutes} min`;
|
||||||
} else if (status?.label && status.label.includes('+')) {
|
} else if (status?.label && status.label.includes('+')) {
|
||||||
delayVal = status.label;
|
const m = status.label.match(/\+(\d+)\s*(?:min)?/);
|
||||||
|
if (m) delayVal = formatDelayVal(parseInt(m[1], 10));
|
||||||
|
else delayVal = status.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
let delayDisplayText = alternateDelay ? 'retardé' : delayVal;
|
let delayDisplayText = alternateDelay ? 'retardé' : delayVal;
|
||||||
let pillText = isSuppressed ? 'Supprimé' : isDelayed ? delayDisplayText : 'À l\'heure';
|
let pillText = isSuppressed ? 'Supprimé' : isDelayed ? delayDisplayText : 'À l\'heure';
|
||||||
|
|
||||||
|
// Détection des modifications de parcours
|
||||||
|
const { destModif, perturbStopsMap } = detectRouteModification(horaire, perturbations, true, normalizeName);
|
||||||
|
|
||||||
// Calcul des vias
|
// Calcul des vias
|
||||||
const stopsList = getStops(horaire);
|
const stopsList = getStops(horaire);
|
||||||
const viaNames = [];
|
const rawViaStops = [];
|
||||||
if (Array.isArray(stopsList) && stopsList.length > 0) {
|
if (Array.isArray(stopsList) && stopsList.length > 0) {
|
||||||
let isAfterStart = false;
|
let isAfterStart = false;
|
||||||
const hasStartInStops = stopsList.some(s => {
|
const hasStartInStops = stopsList.some(s => {
|
||||||
@@ -1389,11 +1627,26 @@ export default function AFLDeparts(){
|
|||||||
if (norm === normalizeName(destination || '')) {
|
if (norm === normalizeName(destination || '')) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
viaNames.push(sName);
|
rawViaStops.push(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viaItems = rawViaStops.map(s => {
|
||||||
|
const sName = s.gare_nom || s.nom || s.name || s.station_name || (typeof s === 'string' ? s : '');
|
||||||
|
const norm = normalizeName(sName);
|
||||||
|
const pInfo = perturbStopsMap.get(norm);
|
||||||
|
const isSupprime = Boolean(pInfo?.isSupprime || s.is_supprime || s.supprime || s.type === 'arret_supprime');
|
||||||
|
const isAjoute = Boolean(pInfo?.isAjoute || s.is_ajoute || s.ajoute || s.type === 'arret_ajoute');
|
||||||
|
const isModifie = Boolean(pInfo?.isModifie || s.is_modifie || s.modifie);
|
||||||
|
return {
|
||||||
|
name: sName,
|
||||||
|
isSupprime,
|
||||||
|
isAjoute,
|
||||||
|
isModifie
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const renderPillIcon = () => {
|
const renderPillIcon = () => {
|
||||||
if (isSuppressed) {
|
if (isSuppressed) {
|
||||||
return (
|
return (
|
||||||
@@ -1475,16 +1728,44 @@ export default function AFLDeparts(){
|
|||||||
<>
|
<>
|
||||||
<div className="afl-top-row">
|
<div className="afl-top-row">
|
||||||
{isCarTER && <BusIcon size="52px" color="white" style={{ marginRight: '15px' }} />}
|
{isCarTER && <BusIcon size="52px" color="white" style={{ marginRight: '15px' }} />}
|
||||||
|
{destModif ? (
|
||||||
|
alternateDelay ? (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||||
|
<AFLRouteBadge type={destModif.isAdded ? 'added' : 'modified'} size="52px" />
|
||||||
|
<MarqueeText text={destModif.newStation} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||||
|
<AFLRouteBadge type="cancelled" size="52px" />
|
||||||
|
<MarqueeText text={destModif.oldStation} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
<MarqueeText text={destination} className="afl-text-dest" style={{ flex: 'none' }} />
|
<MarqueeText text={destination} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="afl-bottom-row">
|
<div className="afl-bottom-row">
|
||||||
<ViasMarquee viaNames={viaNames} />
|
<ViasMarquee viaNames={viaItems} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||||
{isCarTER && <BusIcon size="49px" color="white" style={{ marginRight: '15px' }} />}
|
{isCarTER && <BusIcon size="49px" color="white" style={{ marginRight: '15px' }} />}
|
||||||
|
{destModif ? (
|
||||||
|
alternateDelay ? (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||||
|
<AFLRouteBadge type={destModif.isAdded ? 'added' : 'modified'} size="46px" />
|
||||||
|
<MarqueeText text={destModif.newStation} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||||
|
<AFLRouteBadge type="cancelled" size="46px" />
|
||||||
|
<MarqueeText text={destModif.oldStation} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
<MarqueeText text={destination} className="afl-text-dest" style={{ flex: 'none' }} />
|
<MarqueeText text={destination} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -335,6 +335,7 @@
|
|||||||
.aff-classiques-arrivees__col-platform {
|
.aff-classiques-arrivees__col-platform {
|
||||||
width: 96px;
|
width: 96px;
|
||||||
min-width: 96px;
|
min-width: 96px;
|
||||||
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -630,7 +630,12 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
|
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
|
||||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay, causeDetails };
|
if (maxDelay != null) {
|
||||||
|
const dH = Math.floor(maxDelay / 60);
|
||||||
|
const dRem = maxDelay % 60;
|
||||||
|
const dStr = maxDelay >= 60 ? (dRem > 0 ? `${dH}h${String(dRem).padStart(2, '0')}` : `${dH}h`) : `${maxDelay}min.`;
|
||||||
|
return { label: `retardé +${dStr}`, detail: '', delayMinutes: maxDelay, causeDetails };
|
||||||
|
}
|
||||||
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
|
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
|
||||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||||
|
|
||||||
@@ -1179,7 +1184,14 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
|||||||
const intervalMs = 15000; // 15 secondes par défaut
|
const intervalMs = 15000; // 15 secondes par défaut
|
||||||
const intervalId = setInterval(fetchData, intervalMs);
|
const intervalId = setInterval(fetchData, intervalMs);
|
||||||
|
|
||||||
const onRtUpdate = () => { if (mounted) fetchData(); };
|
let rtDebounce = null;
|
||||||
|
const onRtUpdate = () => {
|
||||||
|
if (!mounted) return;
|
||||||
|
clearTimeout(rtDebounce);
|
||||||
|
rtDebounce = setTimeout(() => {
|
||||||
|
if (mounted) fetchData();
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.addEventListener('sncf-realtime-updated', onRtUpdate);
|
window.addEventListener('sncf-realtime-updated', onRtUpdate);
|
||||||
}
|
}
|
||||||
@@ -1187,11 +1199,12 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
|||||||
return () => {
|
return () => {
|
||||||
mounted = false;
|
mounted = false;
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
|
clearTimeout(rtDebounce);
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.removeEventListener('sncf-realtime-updated', onRtUpdate);
|
window.removeEventListener('sncf-realtime-updated', onRtUpdate);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
|
}, [gareDisplay, gareParam, gareIdParam, defilementMode]);
|
||||||
|
|
||||||
// Helpers pour perturbations (memoized plus haut)
|
// Helpers pour perturbations (memoized plus haut)
|
||||||
|
|
||||||
|
|||||||
@@ -335,6 +335,7 @@
|
|||||||
.aff-classiques-departs__col-platform {
|
.aff-classiques-departs__col-platform {
|
||||||
width: 96px;
|
width: 96px;
|
||||||
min-width: 96px;
|
min-width: 96px;
|
||||||
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -630,7 +630,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
|
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
|
||||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay, causeDetails };
|
if (maxDelay != null) {
|
||||||
|
const dH = Math.floor(maxDelay / 60);
|
||||||
|
const dRem = maxDelay % 60;
|
||||||
|
const dStr = maxDelay >= 60 ? (dRem > 0 ? `${dH}h${String(dRem).padStart(2, '0')}` : `${dH}h`) : `${maxDelay}min.`;
|
||||||
|
return { label: `retardé +${dStr}`, detail: '', delayMinutes: maxDelay, causeDetails };
|
||||||
|
}
|
||||||
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
|
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
|
||||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||||
|
|
||||||
@@ -1178,7 +1183,14 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
|||||||
const intervalMs = 15000; // 15 secondes par défaut
|
const intervalMs = 15000; // 15 secondes par défaut
|
||||||
const intervalId = setInterval(fetchData, intervalMs);
|
const intervalId = setInterval(fetchData, intervalMs);
|
||||||
|
|
||||||
const onRtUpdate = () => { if (mounted) fetchData(); };
|
let rtDebounce = null;
|
||||||
|
const onRtUpdate = () => {
|
||||||
|
if (!mounted) return;
|
||||||
|
clearTimeout(rtDebounce);
|
||||||
|
rtDebounce = setTimeout(() => {
|
||||||
|
if (mounted) fetchData();
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.addEventListener('sncf-realtime-updated', onRtUpdate);
|
window.addEventListener('sncf-realtime-updated', onRtUpdate);
|
||||||
}
|
}
|
||||||
@@ -1186,11 +1198,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
|||||||
return () => {
|
return () => {
|
||||||
mounted = false;
|
mounted = false;
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
|
clearTimeout(rtDebounce);
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.removeEventListener('sncf-realtime-updated', onRtUpdate);
|
window.removeEventListener('sncf-realtime-updated', onRtUpdate);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
|
}, [gareDisplay, gareParam, gareIdParam, defilementMode]);
|
||||||
|
|
||||||
// Helpers pour perturbations (memoized plus haut)
|
// Helpers pour perturbations (memoized plus haut)
|
||||||
|
|
||||||
|
|||||||
@@ -185,13 +185,25 @@
|
|||||||
color: #1a2b4c !important;
|
color: #1a2b4c !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.eva-arr-status-pill--approaching {
|
||||||
|
background-color: #d7e1f0 !important;
|
||||||
|
border: 1.8px solid #7d95b5 !important;
|
||||||
|
color: #1a2b4c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eva-arr-status-pill--atplatform {
|
||||||
|
background-color: #1a2b4c !important;
|
||||||
|
border: none !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
.eva-arr-status-pill--delayed {
|
.eva-arr-status-pill--delayed {
|
||||||
background-color: #ffa74f !important;
|
background-color: #ffa74f !important;
|
||||||
color: #000c2f !important;
|
color: #000c2f !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eva-arr-status-pill--suppressed {
|
.eva-arr-status-pill--suppressed {
|
||||||
background-color: #ff5c73 !important;
|
background-color: #ff4d6d !important;
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,20 +254,28 @@
|
|||||||
text-overflow: ellipsis !important;
|
text-overflow: ellipsis !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Badge Motif de Perturbation (ex: Embouteillages) */
|
/* Badge Motif de Perturbation */
|
||||||
.eva-arr-perturb-badge {
|
.eva-arr-perturb-badge {
|
||||||
display: inline-flex !important;
|
display: inline-flex !important;
|
||||||
align-items: center !important;
|
align-items: center !important;
|
||||||
gap: 8px !important;
|
gap: 8px !important;
|
||||||
background-color: #ffa74f !important;
|
|
||||||
color: #000c2f !important;
|
|
||||||
padding: 4px 15px !important;
|
padding: 4px 15px !important;
|
||||||
border-radius: 6px !important;
|
border-radius: 6px !important;
|
||||||
height: 37px !important;
|
height: 38px !important;
|
||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
flex-shrink: 0 !important;
|
flex-shrink: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.eva-arr-perturb-badge--delayed {
|
||||||
|
background-color: #ffa74f !important;
|
||||||
|
color: #000c2f !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eva-arr-perturb-badge--suppressed {
|
||||||
|
background-color: #d63031 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
.eva-arr-perturb-badge-icon {
|
.eva-arr-perturb-badge-icon {
|
||||||
width: 26px !important;
|
width: 26px !important;
|
||||||
height: 26px !important;
|
height: 26px !important;
|
||||||
@@ -263,12 +283,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.eva-arr-perturb-badge-text {
|
.eva-arr-perturb-badge-text {
|
||||||
font-size: 24px !important;
|
font-size: 22px !important;
|
||||||
font-weight: 700 !important;
|
font-weight: 700 !important;
|
||||||
line-height: 1 !important;
|
line-height: 1.1 !important;
|
||||||
white-space: nowrap !important;
|
white-space: nowrap !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.eva-arr-vias-direct {
|
||||||
|
font-size: 38px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
color: #00c284 !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eva-arr-vias-stop--cancelled {
|
||||||
|
color: #8b9bb4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Section Vias */
|
/* Section Vias */
|
||||||
.eva-arr-vias-container {
|
.eva-arr-vias-container {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
|
|||||||
@@ -178,6 +178,41 @@ const EVACrossIcon = ({ size = '27px', color = 'currentColor', style = {} }) =>
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Icône information carrée orange (Badge motif retard EVA Image 1)
|
||||||
|
const EVAPerturbInfoSquare = ({ size = '36px' }) => (
|
||||||
|
<svg viewBox="0 0 36 36" width={size} height={size} style={{ flexShrink: 0 }}>
|
||||||
|
<rect width="36" height="36" rx="6" fill="#fca758" />
|
||||||
|
<circle cx="18" cy="18" r="10" stroke="#000c2f" strokeWidth="2.2" fill="none" />
|
||||||
|
<circle cx="18" cy="13" r="1.5" fill="#000c2f" />
|
||||||
|
<path d="M16.5 17h2v6h2" stroke="#000c2f" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Icône triangle rouge alerte (Badge motif suppression EVA Image 1)
|
||||||
|
const EVAPerturbWarningSquare = ({ size = '36px' }) => (
|
||||||
|
<svg viewBox="0 0 36 36" width={size} height={size} style={{ flexShrink: 0 }}>
|
||||||
|
<rect width="36" height="36" rx="6" fill="#e63946" />
|
||||||
|
<path d="M18 7l11 19H7L18 7z" stroke="#ffffff" strokeWidth="2.2" fill="none" strokeLinejoin="round" />
|
||||||
|
<line x1="18" y1="14" x2="18" y2="19" stroke="#ffffff" strokeWidth="2.4" strokeLinecap="round" />
|
||||||
|
<circle cx="18" cy="22.5" r="1.3" fill="#ffffff" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Icône Train barré en rouge (Boîte Quai pour train supprimé EVA Image 1, 2, 3)
|
||||||
|
const EVACancelledTrainIcon = ({ size = '60px' }) => (
|
||||||
|
<svg viewBox="0 0 64 64" width={size} height={size} style={{ flexShrink: 0 }}>
|
||||||
|
<rect x="18" y="10" width="28" height="36" rx="7" fill="#ffffff" />
|
||||||
|
<rect x="22" y="15" width="20" height="11" rx="3" fill="#000c2f" />
|
||||||
|
<circle cx="24" cy="35" r="2.8" fill="#000c2f" />
|
||||||
|
<circle cx="40" cy="35" r="2.8" fill="#000c2f" />
|
||||||
|
<rect x="28" y="38" width="8" height="2" rx="1" fill="#000c2f" />
|
||||||
|
<circle cx="21" cy="46" r="2.2" fill="#ffffff" />
|
||||||
|
<circle cx="43" cy="46" r="2.2" fill="#ffffff" />
|
||||||
|
<line x1="16" y1="48" x2="48" y2="48" stroke="#ffffff" strokeWidth="2.5" strokeLinecap="round" />
|
||||||
|
<line x1="8" y1="56" x2="56" y2="8" stroke="#ff3b53" strokeWidth="5.5" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
// Défilement des gares de passage (Vias)
|
// Défilement des gares de passage (Vias)
|
||||||
const EVAViasMarquee = ({ viaNames }) => {
|
const EVAViasMarquee = ({ viaNames }) => {
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
@@ -206,7 +241,7 @@ const EVAViasMarquee = ({ viaNames }) => {
|
|||||||
if (!viaNames || viaNames.length === 0) {
|
if (!viaNames || viaNames.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="eva-arr-vias-container">
|
<div className="eva-arr-vias-container">
|
||||||
<span className="eva-arr-vias-stop">Direct</span>
|
<span className="eva-arr-vias-direct">train direct</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -222,12 +257,24 @@ const EVAViasMarquee = ({ viaNames }) => {
|
|||||||
animationDuration: isOverflowing ? `${duration}s` : undefined
|
animationDuration: isOverflowing ? `${duration}s` : undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{viaNames.map((v, i) => (
|
{viaNames.map((v, i) => {
|
||||||
|
const isObj = typeof v === 'object' && v !== null;
|
||||||
|
const name = isObj ? v.nom || v.name : v;
|
||||||
|
const isCancelled = isObj && Boolean(v.isCancelled || v.cancelled || v.supprime);
|
||||||
|
return (
|
||||||
<React.Fragment key={i}>
|
<React.Fragment key={i}>
|
||||||
{i > 0 && <span className="eva-arr-vias-bullet">•</span>}
|
{i > 0 && <span className="eva-arr-vias-bullet">•</span>}
|
||||||
<span className="eva-arr-vias-stop">{v}</span>
|
{isCancelled ? (
|
||||||
|
<span className="eva-arr-vias-stop eva-arr-vias-stop--cancelled">
|
||||||
|
<span style={{ textDecoration: 'line-through' }}>{name}</span>
|
||||||
|
<span style={{ color: '#e63946', fontWeight: 800, marginLeft: '6px' }}>✕</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="eva-arr-vias-stop">{name}</span>
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -809,7 +856,12 @@ export default function EVAArrivees() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
|
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
|
||||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay, causeDetails };
|
if (maxDelay != null) {
|
||||||
|
const dH = Math.floor(maxDelay / 60);
|
||||||
|
const dRem = maxDelay % 60;
|
||||||
|
const dStr = maxDelay >= 60 ? (dRem > 0 ? `${dH}h${String(dRem).padStart(2, '0')}` : `${dH}h`) : `${maxDelay}min.`;
|
||||||
|
return { label: `retardé +${dStr}`, detail: '', delayMinutes: maxDelay, causeDetails };
|
||||||
|
}
|
||||||
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
|
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
|
||||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||||
|
|
||||||
@@ -1230,7 +1282,7 @@ export default function EVAArrivees() {
|
|||||||
fetchData();
|
fetchData();
|
||||||
const intervalId = setInterval(fetchData, 15000);
|
const intervalId = setInterval(fetchData, 15000);
|
||||||
return () => { mounted = false; clearInterval(intervalId); };
|
return () => { mounted = false; clearInterval(intervalId); };
|
||||||
}, [gareDisplay, gareParam, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]);
|
}, [gareDisplay, gareParam, gareIdParam]);
|
||||||
|
|
||||||
// Alternance retard / texte toutes les 2s
|
// Alternance retard / texte toutes les 2s
|
||||||
const [alternateDelay, setAlternateDelay] = useState(false);
|
const [alternateDelay, setAlternateDelay] = useState(false);
|
||||||
@@ -1309,6 +1361,15 @@ export default function EVAArrivees() {
|
|||||||
return null;
|
return null;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const formatTrainNumber = (num) => {
|
||||||
|
if (!num) return '';
|
||||||
|
const s = String(num).trim();
|
||||||
|
if (/^\d{4,6}$/.test(s)) {
|
||||||
|
return s.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="eva-arr-container">
|
<div className="eva-arr-container">
|
||||||
{/* HEADER */}
|
{/* HEADER */}
|
||||||
@@ -1316,9 +1377,9 @@ export default function EVAArrivees() {
|
|||||||
<div className="eva-arr-header-left">
|
<div className="eva-arr-header-left">
|
||||||
<EVAArrivalsHeaderIcon className="eva-arr-header-icon" />
|
<EVAArrivalsHeaderIcon className="eva-arr-header-icon" />
|
||||||
<div className="eva-arr-header-titles">
|
<div className="eva-arr-header-titles">
|
||||||
<span className="eva-arr-header-title-main">Arrivées</span>
|
<span className="eva-arr-header-title-main">ARRIVÉES</span>
|
||||||
<span className="eva-arr-header-slash">/</span>
|
<span className="eva-arr-header-slash">/</span>
|
||||||
<span className="eva-arr-header-title-sub">Arrivals From / Ankünfte Aus</span>
|
<span className="eva-arr-header-title-sub">ARRIVALS FROM / LLEGADAS DE</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1350,7 +1411,7 @@ export default function EVAArrivees() {
|
|||||||
horaire.voie ||
|
horaire.voie ||
|
||||||
horaire.track ||
|
horaire.track ||
|
||||||
''
|
''
|
||||||
).toString().trim();
|
).toString().replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||||
|
|
||||||
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
const isSub = (travauxSubstitutions || []).some(ts => String(ts.horaire_id) === String(horaire.id) || String(ts.horaire_id) === String(horaire.numero_train) || String(ts.horaire_id) === String(horaire.numero));
|
||||||
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
|
const isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
|
||||||
@@ -1367,19 +1428,42 @@ export default function EVAArrivees() {
|
|||||||
|
|
||||||
let delayVal = '+10 min';
|
let delayVal = '+10 min';
|
||||||
if (typeof status?.delayMinutes === 'number' && status.delayMinutes > 0) {
|
if (typeof status?.delayMinutes === 'number' && status.delayMinutes > 0) {
|
||||||
delayVal = `+${status.delayMinutes} min`;
|
delayVal = `+ ${status.delayMinutes} min`;
|
||||||
} else if (status?.delayMinutes) {
|
} else if (status?.delayMinutes) {
|
||||||
delayVal = `+${status.delayMinutes} min`;
|
delayVal = `+ ${status.delayMinutes} min`;
|
||||||
} else if (status?.label && status.label.includes('+')) {
|
} else if (status?.label && status.label.includes('+')) {
|
||||||
const m = status.label.match(/\+(\d+)\s*(?:min)?/);
|
const m = status.label.match(/\+(\d+)\s*(?:min)?/);
|
||||||
if (m) delayVal = `+${m[1]} min`;
|
if (m) delayVal = `+ ${m[1]} min`;
|
||||||
else delayVal = status.label;
|
else delayVal = status.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pillText = isSuppressed ? 'supprimé' : isDelayed ? (alternateDelay ? 'retardé' : delayVal) : 'à l\'heure';
|
const timeHM = horaire._arrHM || 0;
|
||||||
const pillModifier = isSuppressed ? 'suppressed' : isDelayed ? 'delayed' : 'ontime';
|
const isAtPlatform = !isSuppressed && !isDelayed && timeHM === currentHM;
|
||||||
|
const isApproaching = !isSuppressed && !isDelayed && (timeHM === currentHM + 1 || timeHM === currentHM + 2);
|
||||||
|
|
||||||
// Motif de perturbation (ex: Embouteillages, Travaux, etc.)
|
let pillText = "à l'heure";
|
||||||
|
let pillModifier = 'ontime';
|
||||||
|
let showClockIcon = true;
|
||||||
|
|
||||||
|
if (isSuppressed) {
|
||||||
|
pillText = alternateDelay ? 'Train' : 'supprimé';
|
||||||
|
pillModifier = 'suppressed';
|
||||||
|
showClockIcon = true;
|
||||||
|
} else if (isDelayed) {
|
||||||
|
pillText = alternateDelay ? 'retardé' : delayVal;
|
||||||
|
pillModifier = 'delayed';
|
||||||
|
showClockIcon = true;
|
||||||
|
} else if (isAtPlatform) {
|
||||||
|
pillText = 'à quai';
|
||||||
|
pillModifier = 'atplatform';
|
||||||
|
showClockIcon = false;
|
||||||
|
} else if (isApproaching) {
|
||||||
|
pillText = "à l'approche";
|
||||||
|
pillModifier = 'approaching';
|
||||||
|
showClockIcon = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Motif de perturbation (ex: Défaut alimentation électrique, Train en panne, etc.)
|
||||||
const perturbationReason = decodeText(status?.causeDetails || (isDelayed && !status?.causeDetails ? 'Retard' : ''));
|
const perturbationReason = decodeText(status?.causeDetails || (isDelayed && !status?.causeDetails ? 'Retard' : ''));
|
||||||
|
|
||||||
// Calcul des gares intermédiaires (Vias)
|
// Calcul des gares intermédiaires (Vias)
|
||||||
@@ -1392,7 +1476,8 @@ export default function EVAArrivees() {
|
|||||||
const norm = normalizeName(sName);
|
const norm = normalizeName(sName);
|
||||||
if (norm === normalizeName(origin || '')) continue;
|
if (norm === normalizeName(origin || '')) continue;
|
||||||
if (norm === normalizeName(stationName || '')) break;
|
if (norm === normalizeName(stationName || '')) break;
|
||||||
viaNames.push(decodeText(sName));
|
const isCancelledStop = s.is_cancelled || s.supprime || s.type === 'arret_supprime';
|
||||||
|
viaNames.push({ nom: decodeText(sName), isCancelled: isCancelledStop });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1404,10 +1489,11 @@ export default function EVAArrivees() {
|
|||||||
<div className="eva-arr-col-time">
|
<div className="eva-arr-col-time">
|
||||||
<div className="eva-arr-time-value">{time}</div>
|
<div className="eva-arr-time-value">{time}</div>
|
||||||
<div className={`eva-arr-status-pill eva-arr-status-pill--${pillModifier}`}>
|
<div className={`eva-arr-status-pill eva-arr-status-pill--${pillModifier}`}>
|
||||||
{isSuppressed ? (
|
{showClockIcon && (
|
||||||
<EVACrossIcon className="eva-arr-status-icon" color="#ffffff" />
|
<EVAClockIcon
|
||||||
) : (
|
className="eva-arr-status-icon"
|
||||||
<EVAClockIcon className="eva-arr-status-icon" color={isDelayed ? '#000c2f' : '#1a2b4c'} />
|
color={isSuppressed ? '#ffffff' : isDelayed ? '#000c2f' : '#1a2b4c'}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="eva-arr-status-text">{pillText}</span>
|
<span className="eva-arr-status-text">{pillText}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1418,10 +1504,13 @@ export default function EVAArrivees() {
|
|||||||
<div className="eva-arr-origin-row">
|
<div className="eva-arr-origin-row">
|
||||||
<MarqueeText text={decodedOrigin} className="eva-arr-origin-name" style={{ flex: 'none' }} />
|
<MarqueeText text={decodedOrigin} className="eva-arr-origin-name" style={{ flex: 'none' }} />
|
||||||
{perturbationReason && (
|
{perturbationReason && (
|
||||||
<div className="eva-arr-perturb-badge">
|
alternateDelay ? (
|
||||||
<EVAInfoIcon className="eva-arr-perturb-badge-icon" />
|
<div className={`eva-arr-perturb-badge ${isSuppressed ? 'eva-arr-perturb-badge--suppressed' : 'eva-arr-perturb-badge--delayed'}`}>
|
||||||
<span className="eva-arr-perturb-badge-text">{perturbationReason}</span>
|
<span className="eva-arr-perturb-badge-text">{perturbationReason}</span>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
isSuppressed ? <EVAPerturbWarningSquare /> : <EVAPerturbInfoSquare />
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<EVAViasMarquee viaNames={viaNames} />
|
<EVAViasMarquee viaNames={viaNames} />
|
||||||
@@ -1429,11 +1518,15 @@ export default function EVAArrivees() {
|
|||||||
|
|
||||||
{/* SECTION DROITE : MODE / VOIE & BLOC OPERATEUR/NUMERO */}
|
{/* SECTION DROITE : MODE / VOIE & BLOC OPERATEUR/NUMERO */}
|
||||||
<div className="eva-arr-col-right">
|
<div className="eva-arr-col-right">
|
||||||
{isSub || isCarTER ? (
|
{isSuppressed ? (
|
||||||
|
<div className="eva-arr-voie-box">
|
||||||
|
<EVACancelledTrainIcon />
|
||||||
|
</div>
|
||||||
|
) : (isSub || isCarTER) ? (
|
||||||
<div className="eva-arr-bus-box">
|
<div className="eva-arr-bus-box">
|
||||||
<EVABusIcon className="eva-arr-bus-icon" />
|
<EVABusIcon className="eva-arr-bus-icon" />
|
||||||
</div>
|
</div>
|
||||||
) : (rawPlatform && !isSuppressed) ? (
|
) : rawPlatform ? (
|
||||||
<div className="eva-arr-voie-box">
|
<div className="eva-arr-voie-box">
|
||||||
<span className="eva-arr-voie-label">Voie</span>
|
<span className="eva-arr-voie-label">Voie</span>
|
||||||
<span className="eva-arr-voie-val">{rawPlatform}</span>
|
<span className="eva-arr-voie-val">{rawPlatform}</span>
|
||||||
@@ -1442,7 +1535,7 @@ export default function EVAArrivees() {
|
|||||||
|
|
||||||
<div className="eva-arr-train-block">
|
<div className="eva-arr-train-block">
|
||||||
<span className="eva-arr-train-op">{op}</span>
|
<span className="eva-arr-train-op">{op}</span>
|
||||||
<span className="eva-arr-train-num">{number}</span>
|
<span className="eva-arr-train-num">{formatTrainNumber(number)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -805,7 +805,12 @@ export default function EVADeparts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
|
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
|
||||||
if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay, causeDetails };
|
if (maxDelay != null) {
|
||||||
|
const dH = Math.floor(maxDelay / 60);
|
||||||
|
const dRem = maxDelay % 60;
|
||||||
|
const dStr = maxDelay >= 60 ? (dRem > 0 ? `${dH}h${String(dRem).padStart(2, '0')}` : `${dH}h`) : `${maxDelay}min.`;
|
||||||
|
return { label: `retardé +${dStr}`, detail: '', delayMinutes: maxDelay, causeDetails };
|
||||||
|
}
|
||||||
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
|
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
|
||||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||||
|
|
||||||
@@ -1243,7 +1248,7 @@ export default function EVADeparts() {
|
|||||||
fetchData();
|
fetchData();
|
||||||
const intervalId = setInterval(fetchData, 15000);
|
const intervalId = setInterval(fetchData, 15000);
|
||||||
return () => { mounted = false; clearInterval(intervalId); };
|
return () => { mounted = false; clearInterval(intervalId); };
|
||||||
}, [gareDisplay, gareParam, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]);
|
}, [gareDisplay, gareParam, gareIdParam]);
|
||||||
|
|
||||||
// Alternance retard / texte toutes les 2s
|
// Alternance retard / texte toutes les 2s
|
||||||
const [alternateDelay, setAlternateDelay] = useState(false);
|
const [alternateDelay, setAlternateDelay] = useState(false);
|
||||||
|
|||||||
+155
-57
@@ -1,27 +1,34 @@
|
|||||||
|
|
||||||
let isRealtimeRefreshing = false;
|
let isRealtimeRefreshing = false;
|
||||||
|
|
||||||
// Fonctions de persistance du cache temps réel dans sessionStorage
|
// Fonctions de persistance du cache temps réel dans localStorage et sessionStorage
|
||||||
function saveRealtimeCacheToStorage() {
|
function saveRealtimeCacheToStorage() {
|
||||||
if (typeof window === 'undefined' || !window.sessionStorage) return;
|
if (typeof window === 'undefined') return;
|
||||||
try {
|
try {
|
||||||
if (siriLiteCache.platforms && siriLiteCache.platforms.size > 0) {
|
if (siriLiteCache.platforms && siriLiteCache.platforms.size > 0) {
|
||||||
const arr = Array.from(siriLiteCache.platforms.entries()).slice(0, 4000);
|
const arr = Array.from(siriLiteCache.platforms.entries()).slice(0, 4000);
|
||||||
window.sessionStorage.setItem('sncf_siri_platforms', JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
ts: siriLiteCache.timestamp,
|
ts: siriLiteCache.timestamp,
|
||||||
data: arr
|
data: arr
|
||||||
}));
|
});
|
||||||
|
if (window.localStorage) {
|
||||||
|
window.localStorage.setItem('sncf_siri_platforms', payload);
|
||||||
|
}
|
||||||
|
if (window.sessionStorage) {
|
||||||
|
window.sessionStorage.setItem('sncf_siri_platforms', payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreRealtimeCacheFromStorage() {
|
function restoreRealtimeCacheFromStorage() {
|
||||||
if (typeof window === 'undefined' || !window.sessionStorage) return;
|
if (typeof window === 'undefined') return;
|
||||||
try {
|
try {
|
||||||
const raw = window.sessionStorage.getItem('sncf_siri_platforms');
|
const raw = (window.localStorage && window.localStorage.getItem('sncf_siri_platforms')) ||
|
||||||
|
(window.sessionStorage && window.sessionStorage.getItem('sncf_siri_platforms'));
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
if (parsed && Array.isArray(parsed.data) && (Date.now() - parsed.ts < 900000)) {
|
if (parsed && Array.isArray(parsed.data) && (Date.now() - parsed.ts < 1800000)) {
|
||||||
siriLiteCache.timestamp = parsed.ts;
|
siriLiteCache.timestamp = parsed.ts;
|
||||||
siriLiteCache.platforms = new Map(parsed.data);
|
siriLiteCache.platforms = new Map(parsed.data);
|
||||||
}
|
}
|
||||||
@@ -29,16 +36,23 @@ function restoreRealtimeCacheFromStorage() {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function triggerRealtimeBackgroundRefresh() {
|
let lastRealtimeTriggerTime = 0;
|
||||||
if (isRealtimeRefreshing) return;
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
const needRt = (now - gtfsRtCache.timestamp > 30000) || gtfsRtCache.updates.size === 0;
|
let activeRealtimePromise = null;
|
||||||
const needSiri = (now - siriLiteCache.timestamp > 60000) || siriLiteCache.platforms.size === 0;
|
|
||||||
const needSx = (now - situationExchangeCache.timestamp > 60000) || situationExchangeCache.platforms.size === 0;
|
export async function triggerRealtimeBackgroundRefresh(force = false) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (activeRealtimePromise) return activeRealtimePromise;
|
||||||
|
if (!force && (now - lastRealtimeTriggerTime < 30000)) return;
|
||||||
|
lastRealtimeTriggerTime = now;
|
||||||
|
|
||||||
|
const needRt = force || (now - gtfsRtCache.timestamp > 30000) || gtfsRtCache.updates.size === 0;
|
||||||
|
const needSiri = force || (now - siriLiteCache.timestamp > 60000) || siriLiteCache.platforms.size === 0;
|
||||||
|
const needSx = force || (now - situationExchangeCache.timestamp > 60000) || situationExchangeCache.platforms.size === 0;
|
||||||
|
|
||||||
if (!needRt && !needSiri && !needSx) return;
|
if (!needRt && !needSiri && !needSx) return;
|
||||||
|
|
||||||
|
activeRealtimePromise = (async () => {
|
||||||
isRealtimeRefreshing = true;
|
isRealtimeRefreshing = true;
|
||||||
try {
|
try {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
@@ -51,10 +65,14 @@ export async function triggerRealtimeBackgroundRefresh() {
|
|||||||
// ignore
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
isRealtimeRefreshing = false;
|
isRealtimeRefreshing = false;
|
||||||
|
activeRealtimePromise = null;
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.dispatchEvent(new CustomEvent('sncf-realtime-updated'));
|
window.dispatchEvent(new CustomEvent('sncf-realtime-updated'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return activeRealtimePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
@@ -204,37 +222,14 @@ function extractAllTrainNumbers(journeyXml) {
|
|||||||
return Array.from(nums);
|
return Array.from(nums);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère et décode le flux SIRI Lite SNCF officiel en temps réel (circulations dans les 60 prochaines minutes)
|
|
||||||
* Source : https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-estimated-timetable
|
|
||||||
*/
|
|
||||||
async function fetchSiriLiteData() {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - siriLiteCache.timestamp < REALTIME_CACHE_TTL && siriLiteCache.platforms.size > 0) {
|
|
||||||
return siriLiteCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
const platformsMap = new Map();
|
|
||||||
const updatesMap = new Map();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetchWithTimeout(SNCF_SIRI_LITE_TIMETABLE, {
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/xml, text/xml, */*'
|
|
||||||
}
|
|
||||||
}, 35000);
|
|
||||||
|
|
||||||
if (res && res.ok) {
|
|
||||||
const xml = await res.text();
|
|
||||||
const journeys = xml.split('<EstimatedVehicleJourney>');
|
|
||||||
|
|
||||||
for (let i = 1; i < journeys.length; i++) {
|
|
||||||
const j = journeys[i];
|
|
||||||
|
|
||||||
|
function processJourneyXml(j, platformsMap, updatesMap) {
|
||||||
// 1. Extraction exhaustive des identifiants et numéros de train
|
// 1. Extraction exhaustive des identifiants et numéros de train
|
||||||
const trainNums = extractAllTrainNumbers(j);
|
const trainNums = extractAllTrainNumbers(j);
|
||||||
|
|
||||||
const journeyRefMatch = j.match(/<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/) || j.match(/<VehicleJourneyRef>([^<]+)<\/VehicleJourneyRef>/) || j.match(/<FramedVehicleJourneyRef>[\s\S]*?<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/);
|
const journeyRefMatch = j.match(/<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/) ||
|
||||||
|
j.match(/<VehicleJourneyRef>([^<]+)<\/VehicleJourneyRef>/) ||
|
||||||
|
j.match(/<FramedVehicleJourneyRef>[\s\S]*?<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/);
|
||||||
const journeyRef = journeyRefMatch ? journeyRefMatch[1].trim() : '';
|
const journeyRef = journeyRefMatch ? journeyRefMatch[1].trim() : '';
|
||||||
|
|
||||||
const isCanceledJourney = j.includes('<Cancellation>true</Cancellation>') || j.includes('<JourneyStatus>cancelled</JourneyStatus>');
|
const isCanceledJourney = j.includes('<Cancellation>true</Cancellation>') || j.includes('<JourneyStatus>cancelled</JourneyStatus>');
|
||||||
@@ -288,19 +283,32 @@ async function fetchSiriLiteData() {
|
|||||||
|
|
||||||
if (platform) {
|
if (platform) {
|
||||||
for (const tNum of trainNums) {
|
for (const tNum of trainNums) {
|
||||||
|
platformsMap.set(tNum, platform);
|
||||||
|
const ct = cleanNum(tNum);
|
||||||
|
if (ct) platformsMap.set(ct, platform);
|
||||||
if (uic) {
|
if (uic) {
|
||||||
platformsMap.set(`${tNum}_${uic}`, platform);
|
platformsMap.set(`${tNum}_${uic}`, platform);
|
||||||
platformsMap.set(`${uic}_${tNum}`, platform);
|
platformsMap.set(`${uic}_${tNum}`, platform);
|
||||||
|
if (ct) {
|
||||||
|
platformsMap.set(`${ct}_${uic}`, platform);
|
||||||
|
platformsMap.set(`${uic}_${ct}`, platform);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (uic7) {
|
if (uic7) {
|
||||||
platformsMap.set(`${tNum}_${uic7}`, platform);
|
platformsMap.set(`${tNum}_${uic7}`, platform);
|
||||||
platformsMap.set(`${uic7}_${tNum}`, platform);
|
platformsMap.set(`${uic7}_${tNum}`, platform);
|
||||||
|
if (ct) {
|
||||||
|
platformsMap.set(`${ct}_${uic7}`, platform);
|
||||||
|
platformsMap.set(`${uic7}_${ct}`, platform);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (stopNameClean) {
|
if (stopNameClean) {
|
||||||
platformsMap.set(`${tNum}_${stopNameClean}`, platform);
|
platformsMap.set(`${tNum}_${stopNameClean}`, platform);
|
||||||
|
if (ct) platformsMap.set(`${ct}_${stopNameClean}`, platform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (journeyRef) {
|
if (journeyRef) {
|
||||||
|
platformsMap.set(journeyRef, platform);
|
||||||
if (uic) {
|
if (uic) {
|
||||||
platformsMap.set(`${journeyRef}_${uic}`, platform);
|
platformsMap.set(`${journeyRef}_${uic}`, platform);
|
||||||
platformsMap.set(`${uic}_${journeyRef}`, platform);
|
platformsMap.set(`${uic}_${journeyRef}`, platform);
|
||||||
@@ -326,19 +334,96 @@ async function fetchSiriLiteData() {
|
|||||||
updatesMap.set(journeyRef, info);
|
updatesMap.set(journeyRef, info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère et décode le flux SIRI Lite SNCF officiel en temps réel (circulations dans les 60 prochaines minutes)
|
||||||
|
* Source : https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-estimated-timetable
|
||||||
|
* Décodage en streaming pour apparition instantanée des quais (sans attendre les 22 Mo complets)
|
||||||
|
*/
|
||||||
|
async function fetchSiriLiteData() {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - siriLiteCache.timestamp < REALTIME_CACHE_TTL && siriLiteCache.platforms.size > 0) {
|
||||||
|
return siriLiteCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conserver les quais déjà connus pour enrichir progressivement
|
||||||
|
const platformsMap = new Map(siriLiteCache.platforms || []);
|
||||||
|
const updatesMap = new Map(siriLiteCache.updates || []);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetchWithTimeout(SNCF_SIRI_LITE_TIMETABLE, {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/xml, text/xml, */*'
|
||||||
|
}
|
||||||
|
}, 45000);
|
||||||
|
|
||||||
|
if (res && res.ok) {
|
||||||
|
let lastDispatchTime = Date.now();
|
||||||
|
let newPlatformsCount = 0;
|
||||||
|
|
||||||
|
if (res.body && typeof res.body.getReader === 'function') {
|
||||||
|
const reader = res.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
let idx;
|
||||||
|
while ((idx = buffer.indexOf('</EstimatedVehicleJourney>')) !== -1) {
|
||||||
|
const journeyXml = buffer.slice(0, idx + 26);
|
||||||
|
buffer = buffer.slice(idx + 26);
|
||||||
|
const beforeSize = platformsMap.size;
|
||||||
|
processJourneyXml(journeyXml, platformsMap, updatesMap);
|
||||||
|
if (platformsMap.size > beforeSize) {
|
||||||
|
newPlatformsCount += (platformsMap.size - beforeSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mise à jour continue du cache mémoire et stockage sans provoquer de re-fetch intempestif
|
||||||
|
const nowTime = Date.now();
|
||||||
|
if (newPlatformsCount > 0 && (nowTime - lastDispatchTime > 1500)) {
|
||||||
|
lastDispatchTime = nowTime;
|
||||||
|
newPlatformsCount = 0;
|
||||||
|
siriLiteCache = {
|
||||||
|
timestamp: nowTime,
|
||||||
|
platforms: new Map(platformsMap),
|
||||||
|
updates: new Map(updatesMap)
|
||||||
|
};
|
||||||
|
saveRealtimeCacheToStorage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try { reader.releaseLock(); } catch (_) {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback sans Web Streams
|
||||||
|
const xml = await res.text();
|
||||||
|
const journeys = xml.split('<EstimatedVehicleJourney>');
|
||||||
|
for (let i = 1; i < journeys.length; i++) {
|
||||||
|
processJourneyXml(journeys[i], platformsMap, updatesMap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platformsMap.size > 0 || updatesMap.size > 0) {
|
if (platformsMap.size > 0 || updatesMap.size > 0) {
|
||||||
siriLiteCache = {
|
siriLiteCache = {
|
||||||
timestamp: now,
|
timestamp: Date.now(),
|
||||||
platforms: platformsMap,
|
platforms: platformsMap,
|
||||||
updates: updatesMap
|
updates: updatesMap
|
||||||
};
|
};
|
||||||
saveRealtimeCacheToStorage();
|
saveRealtimeCacheToStorage();
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.dispatchEvent(new CustomEvent('sncf-realtime-updated'));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('[sncfApi] Notice: SIRI-Lite Estimated Timetable refresh error/timeout:', err);
|
console.warn('[sncfApi] Notice: SIRI-Lite Estimated Timetable stream refresh:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
return siriLiteCache;
|
return siriLiteCache;
|
||||||
@@ -669,32 +754,32 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
|||||||
const publicUrl = (typeof process !== 'undefined' && process.env && process.env.PUBLIC_URL) ? process.env.PUBLIC_URL : '';
|
const publicUrl = (typeof process !== 'undefined' && process.env && process.env.PUBLIC_URL) ? process.env.PUBLIC_URL : '';
|
||||||
const stationUrl = `${publicUrl}/data/stations/${uic}.json`;
|
const stationUrl = `${publicUrl}/data/stations/${uic}.json`;
|
||||||
|
|
||||||
// Récupération avec cache mémoire immédiat pour affichage instantané en dev
|
// Chargement simultané de toutes les données : Horaires + GTFS-RT + SIRI-LITE
|
||||||
let data = stationJsonCache.get(String(uic));
|
const fetchStationJson = async () => {
|
||||||
if (!data) {
|
let cached = stationJsonCache.get(String(uic));
|
||||||
|
if (cached) return cached;
|
||||||
const jsonRes = await fetch(stationUrl).catch(err => {
|
const jsonRes = await fetch(stationUrl).catch(err => {
|
||||||
console.warn(`[sncfApi] Notice: fetch station ${uic} error:`, err);
|
console.warn(`[sncfApi] Notice: fetch station ${uic} error:`, err);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
if (jsonRes && jsonRes.ok) {
|
if (jsonRes && jsonRes.ok) {
|
||||||
data = await jsonRes.json();
|
cached = await jsonRes.json();
|
||||||
stationJsonCache.set(String(uic), data);
|
stationJsonCache.set(String(uic), cached);
|
||||||
}
|
return cached;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [stationData] = await Promise.all([
|
||||||
|
fetchStationJson(),
|
||||||
|
triggerRealtimeBackgroundRefresh()
|
||||||
|
]);
|
||||||
|
let data = stationData;
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
console.warn(`[sncfApi] Fichier de gare introuvable : ${stationUrl}`);
|
console.warn(`[sncfApi] Fichier de gare introuvable : ${stationUrl}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utilisation immédiate du cache temps réel & déclenchement non-bloquant en tâche de fond
|
|
||||||
const rtPromise = triggerRealtimeBackgroundRefresh();
|
|
||||||
if (gtfsRtCache.updates.size === 0 && siriLiteCache.platforms.size === 0) {
|
|
||||||
await Promise.race([
|
|
||||||
rtPromise,
|
|
||||||
new Promise(resolve => setTimeout(resolve, 800))
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
const rtData = gtfsRtCache || { updates: new Map(), alerts: [] };
|
const rtData = gtfsRtCache || { updates: new Map(), alerts: [] };
|
||||||
const siriData = siriLiteCache || { platforms: new Map(), updates: new Map() };
|
const siriData = siriLiteCache || { platforms: new Map(), updates: new Map() };
|
||||||
const sxData = situationExchangeCache || { platforms: new Map(), updates: new Map(), alerts: [] };
|
const sxData = situationExchangeCache || { platforms: new Map(), updates: new Map(), alerts: [] };
|
||||||
@@ -789,6 +874,13 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
|||||||
(uicClean && cNum && sxData.platforms.get(`${uicClean}_${cNum}`)) ||
|
(uicClean && cNum && sxData.platforms.get(`${uicClean}_${cNum}`)) ||
|
||||||
(cNum && cName && sxData.platforms.get(`${cNum}_${cName}`)) ||
|
(cNum && cName && sxData.platforms.get(`${cNum}_${cName}`)) ||
|
||||||
(rawNum && cName && sxData.platforms.get(`${rawNum}_${cName}`)) ||
|
(rawNum && cName && sxData.platforms.get(`${rawNum}_${cName}`)) ||
|
||||||
|
// 3. Fallback sur le numéro de train seul si connu dans SIRI-Lite / Situation Exchange
|
||||||
|
(cNum && siriData.platforms.get(cNum)) ||
|
||||||
|
(rawNum && siriData.platforms.get(rawNum)) ||
|
||||||
|
(cNum && siriData.updates.get(cNum)?.platform) ||
|
||||||
|
(rawNum && siriData.updates.get(rawNum)?.platform) ||
|
||||||
|
(cNum && sxData.updates.get(cNum)?.platform) ||
|
||||||
|
(rawNum && sxData.updates.get(rawNum)?.platform) ||
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -841,7 +933,13 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
|||||||
statusLabel = 'supprimé';
|
statusLabel = 'supprimé';
|
||||||
etat = 'supprime';
|
etat = 'supprime';
|
||||||
} else if (delayMinutes > 0) {
|
} else if (delayMinutes > 0) {
|
||||||
|
if (delayMinutes >= 60) {
|
||||||
|
const h = Math.floor(delayMinutes / 60);
|
||||||
|
const rem = delayMinutes % 60;
|
||||||
|
statusLabel = rem > 0 ? `retardé +${h}h${String(rem).padStart(2, '0')}` : `retardé +${h}h`;
|
||||||
|
} else {
|
||||||
statusLabel = `retardé +${delayMinutes}min.`;
|
statusLabel = `retardé +${delayMinutes}min.`;
|
||||||
|
}
|
||||||
etat = 'retarde';
|
etat = 'retarde';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -187,8 +187,8 @@ export function resolveCirculationIdentity(trainNumStr, options = {}) {
|
|||||||
isSubstitution: true,
|
isSubstitution: true,
|
||||||
regionCode: best.region,
|
regionCode: best.region,
|
||||||
brand: meta.brand,
|
brand: meta.brand,
|
||||||
typeLabel: 'CAR',
|
typeLabel: 'CAR TER',
|
||||||
logoFile: 'track-car.svg',
|
logoFile: 'sncf-voyageurs-logo.png',
|
||||||
cssClass: meta.cssClass,
|
cssClass: meta.cssClass,
|
||||||
color: meta.color
|
color: meta.color
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user