Compare commits
4
Commits
06cce59f82
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7334a2d9f7 | ||
|
|
8f00da0896 | ||
|
|
23828a54ef | ||
|
|
eb198cc727 |
No files matched your search
+46
-21
@@ -1,18 +1,6 @@
|
||||
const CACHE_NAME = 'ferrovia-pwa-v1';
|
||||
const CACHE_NAME = 'ferrovia-pwa-v2';
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -28,24 +16,61 @@ self.addEventListener('activate', (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)) {
|
||||
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(
|
||||
caches.match(event.request).then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
return fetch(event.request).then((networkResponse) => {
|
||||
return caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(event.request, networkResponse.clone());
|
||||
return networkResponse;
|
||||
});
|
||||
}).catch(() => {
|
||||
// En cas d'échec réseau total, on pourrait retourner un fallback hors-ligne
|
||||
return caches.match('./index.html');
|
||||
if (networkResponse && networkResponse.ok) {
|
||||
const clone = networkResponse.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
||||
}
|
||||
return networkResponse;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"name": "Aleop"
|
||||
},
|
||||
{
|
||||
"file": "sncf-voyageurs-logo.png",
|
||||
"path": "/sncf-voyageurs-logo.png",
|
||||
"file": "logo-auvergerhonealpes.svg",
|
||||
"path": "/logo-auvergerhonealpes.svg",
|
||||
"slug": "auvergerhonealpes",
|
||||
"name": "TER AURA"
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@ if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js').then(registration => {
|
||||
console.log('SW registered: ', registration);
|
||||
registration.update();
|
||||
}).catch(registrationError => {
|
||||
console.log('SW registration failed: ', registrationError);
|
||||
});
|
||||
|
||||
@@ -55,6 +55,215 @@ const BusIcon = ({ size = '59px', color = 'white', style = {} }) => (
|
||||
</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 containerRef = useRef(null);
|
||||
const textRef = useRef(null);
|
||||
@@ -68,7 +277,6 @@ const ViasMarquee = ({ viaNames }) => {
|
||||
const tWidth = textRef.current.scrollWidth;
|
||||
if (tWidth > cWidth) {
|
||||
setIsOverflowing(true);
|
||||
// Vitesse de défilement rapide (~280px/s)
|
||||
setDuration(Math.max(1.0, tWidth / 280));
|
||||
} else {
|
||||
setIsOverflowing(false);
|
||||
@@ -99,18 +307,38 @@ const ViasMarquee = ({ viaNames }) => {
|
||||
animationDuration: isOverflowing ? `${duration}s` : undefined
|
||||
}}
|
||||
>
|
||||
{viaNames.map((v, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <span className="afl-arr-vias-chevron">></span>}
|
||||
<span className="afl-arr-vias-stop">{v}</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{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}>
|
||||
{i > 0 && <span className="afl-arr-vias-chevron">></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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function Clock(){
|
||||
const [now, setNow] = useState(new Date());
|
||||
useEffect(() => {
|
||||
@@ -629,7 +857,12 @@ export default function AFLArrivees(){
|
||||
}
|
||||
|
||||
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 };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
@@ -1137,7 +1370,7 @@ export default function AFLArrivees(){
|
||||
const intervalId = setInterval(fetchData, intervalMs);
|
||||
|
||||
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)
|
||||
|
||||
@@ -1249,16 +1482,7 @@ export default function AFLArrivees(){
|
||||
{/* HEADER */}
|
||||
<header className="afl-arr-header">
|
||||
<div className="afl-arr-header-left">
|
||||
<svg width="49px" height="49px" viewBox="0 0 28 28" fill="none" style={{ flexShrink: 0 }}>
|
||||
{/* 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>
|
||||
<AFLDebarquementIcon size="49px" color="white" />
|
||||
<div className="afl-arr-header-title">Arrivées</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';
|
||||
|
||||
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) {
|
||||
delayVal = `+ ${status.delayMinutes} min`;
|
||||
delayVal = formatDelayVal(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('+')) {
|
||||
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 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 viaNames = [];
|
||||
const rawPrevStops = [];
|
||||
if (Array.isArray(stopsList)) {
|
||||
for (const s of stopsList) {
|
||||
const sName = s.gare_nom || s.nom || s.name || s.station_name || '';
|
||||
@@ -1347,9 +1585,26 @@ export default function AFLArrivees(){
|
||||
const norm = normalizeName(sName);
|
||||
if (norm === normalizeName(origin || '')) continue;
|
||||
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 = () => {
|
||||
if (isSuppressed) {
|
||||
@@ -1432,16 +1687,44 @@ export default function AFLArrivees(){
|
||||
<>
|
||||
<div className="afl-arr-top-row">
|
||||
{isCarTER && <BusIcon size="52px" color="white" style={{ marginRight: '15px' }} />}
|
||||
<MarqueeText text={origin} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||
{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' }} />
|
||||
)}
|
||||
</div>
|
||||
<div className="afl-arr-bottom-row">
|
||||
<ViasMarquee viaNames={viaNames} />
|
||||
<ViasMarquee viaNames={viaItems} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||
{isCarTER && <BusIcon size="49px" color="white" style={{ marginRight: '15px' }} />}
|
||||
<MarqueeText text={origin} className="afl-arr-text-dest" style={{ flex: 'none' }} />
|
||||
{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' }} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -55,6 +55,215 @@ const BusIcon = ({ size = '59px', color = 'white', style = {} }) => (
|
||||
</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 containerRef = useRef(null);
|
||||
const textRef = useRef(null);
|
||||
@@ -68,7 +277,6 @@ const ViasMarquee = ({ viaNames }) => {
|
||||
const tWidth = textRef.current.scrollWidth;
|
||||
if (tWidth > cWidth) {
|
||||
setIsOverflowing(true);
|
||||
// Vitesse de défilement multipliée par 2 (~280px/s)
|
||||
setDuration(Math.max(1.0, tWidth / 280));
|
||||
} else {
|
||||
setIsOverflowing(false);
|
||||
@@ -99,18 +307,38 @@ const ViasMarquee = ({ viaNames }) => {
|
||||
animationDuration: isOverflowing ? `${duration}s` : undefined
|
||||
}}
|
||||
>
|
||||
{viaNames.map((v, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <span className="afl-vias-chevron">></span>}
|
||||
<span className="afl-vias-stop">{v}</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{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}>
|
||||
{i > 0 && <span className="afl-vias-chevron">></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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// (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)
|
||||
|
||||
@@ -637,7 +865,12 @@ export default function AFLDeparts(){
|
||||
}
|
||||
|
||||
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 };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
@@ -1164,7 +1397,7 @@ export default function AFLDeparts(){
|
||||
const intervalId = setInterval(fetchData, intervalMs);
|
||||
|
||||
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)
|
||||
|
||||
@@ -1276,16 +1509,7 @@ export default function AFLDeparts(){
|
||||
{/* HEADER */}
|
||||
<header className="afl-header">
|
||||
<div className="afl-header-left">
|
||||
<svg width="49px" height="49px" viewBox="0 0 28 28" fill="none" style={{ flexShrink: 0 }}>
|
||||
{/* 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>
|
||||
<AFLEmbarquementIcon size="49px" color="white" />
|
||||
<div className="afl-header-title">Départs</div>
|
||||
</div>
|
||||
|
||||
@@ -1353,20 +1577,34 @@ export default function AFLDeparts(){
|
||||
const pillClass = isSuppressed ? 'afl-pill--suppressed' : isDelayed ? 'afl-pill--delayed' : 'afl-pill--ontime';
|
||||
|
||||
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) {
|
||||
delayVal = `+ ${status.delayMinutes} min`;
|
||||
delayVal = formatDelayVal(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('+')) {
|
||||
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 pillText = isSuppressed ? 'Supprimé' : isDelayed ? delayDisplayText : 'À l\'heure';
|
||||
|
||||
// Détection des modifications de parcours
|
||||
const { destModif, perturbStopsMap } = detectRouteModification(horaire, perturbations, true, normalizeName);
|
||||
|
||||
// Calcul des vias
|
||||
const stopsList = getStops(horaire);
|
||||
const viaNames = [];
|
||||
const rawViaStops = [];
|
||||
if (Array.isArray(stopsList) && stopsList.length > 0) {
|
||||
let isAfterStart = false;
|
||||
const hasStartInStops = stopsList.some(s => {
|
||||
@@ -1389,11 +1627,26 @@ export default function AFLDeparts(){
|
||||
if (norm === normalizeName(destination || '')) {
|
||||
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 = () => {
|
||||
if (isSuppressed) {
|
||||
return (
|
||||
@@ -1475,16 +1728,44 @@ export default function AFLDeparts(){
|
||||
<>
|
||||
<div className="afl-top-row">
|
||||
{isCarTER && <BusIcon size="52px" color="white" style={{ marginRight: '15px' }} />}
|
||||
<MarqueeText text={destination} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||
{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' }} />
|
||||
)}
|
||||
</div>
|
||||
<div className="afl-bottom-row">
|
||||
<ViasMarquee viaNames={viaNames} />
|
||||
<ViasMarquee viaNames={viaItems} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
|
||||
{isCarTER && <BusIcon size="49px" color="white" style={{ marginRight: '15px' }} />}
|
||||
<MarqueeText text={destination} className="afl-text-dest" style={{ flex: 'none' }} />
|
||||
{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' }} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -335,6 +335,7 @@
|
||||
.aff-classiques-arrivees__col-platform {
|
||||
width: 96px;
|
||||
min-width: 96px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -434,3 +435,4 @@
|
||||
margin-top: 1px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
@@ -630,7 +630,12 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
}
|
||||
|
||||
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 };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
@@ -696,12 +701,17 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
let inFlight = false; // empêche les fetchs concurrents
|
||||
let pendingRefresh = false;
|
||||
const source = getSourceFromPath(window.location.pathname);
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!mounted) return;
|
||||
if (inFlight) return;
|
||||
if (inFlight) {
|
||||
pendingRefresh = true;
|
||||
return;
|
||||
}
|
||||
inFlight = true;
|
||||
pendingRefresh = false;
|
||||
|
||||
if (source === 'sncf') {
|
||||
setLoading(true);
|
||||
@@ -742,6 +752,10 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
} finally {
|
||||
if (mounted) setLoading(false);
|
||||
inFlight = false;
|
||||
if (pendingRefresh && mounted) {
|
||||
pendingRefresh = false;
|
||||
fetchData();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1170,8 +1184,27 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
const intervalMs = 15000; // 15 secondes par défaut
|
||||
const intervalId = setInterval(fetchData, intervalMs);
|
||||
|
||||
return () => { mounted = false; clearInterval(intervalId); };
|
||||
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
|
||||
let rtDebounce = null;
|
||||
const onRtUpdate = () => {
|
||||
if (!mounted) return;
|
||||
clearTimeout(rtDebounce);
|
||||
rtDebounce = setTimeout(() => {
|
||||
if (mounted) fetchData();
|
||||
}, 2000);
|
||||
};
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('sncf-realtime-updated', onRtUpdate);
|
||||
}
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
clearInterval(intervalId);
|
||||
clearTimeout(rtDebounce);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('sncf-realtime-updated', onRtUpdate);
|
||||
}
|
||||
};
|
||||
}, [gareDisplay, gareParam, gareIdParam, defilementMode]);
|
||||
|
||||
// Helpers pour perturbations (memoized plus haut)
|
||||
|
||||
@@ -1254,7 +1287,7 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
}
|
||||
|
||||
const horaire = h || {};
|
||||
const { logoSrc, typeLabel, numTrain } = getTrainLogoAndMeta(horaire);
|
||||
const { logoSrc, typeLabel, numTrain, isCar: metaIsCar, isSubstitution: metaIsSubstitution, brand: metaBrand, brandClass } = getTrainLogoAndMeta(horaire, gareDisplay || gareParam);
|
||||
const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_arrivee || horaire.arrival_time || horaire.time || horaire.heure || '');
|
||||
const origin = horaire.gare_depart_nom || horaire.gare_depart || horaire.origine || '';
|
||||
const formattedOrigin = formatStationName(origin);
|
||||
@@ -1275,7 +1308,8 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||
const isSuppressed = status && status.label === 'supprimé';
|
||||
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
|
||||
const isCar = (
|
||||
const isSub = metaIsSubstitution || Boolean(horaire.is_substitution);
|
||||
const isCar = metaIsCar || (
|
||||
(horaire.type_train && (horaire.type_train.toUpperCase().includes('AUTOCAR') || horaire.type_train.toUpperCase().includes('CAR TER') || horaire.type_train.toUpperCase() === 'BUS')) ||
|
||||
(horaire.op && (horaire.op.toUpperCase().includes('AUTOCAR') || horaire.op.toUpperCase().includes('CAR TER') || horaire.op.toUpperCase() === 'BUS')) ||
|
||||
(horaire.type && (horaire.type.toUpperCase().includes('AUTOCAR') || horaire.type.toUpperCase().includes('CAR TER') || horaire.type.toUpperCase() === 'BUS')) ||
|
||||
@@ -1335,7 +1369,7 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-arrivees__col-destination">
|
||||
<MarqueeText text={formattedOrigin} className="aff-classiques-arrivees__destination" />
|
||||
<MarqueeText text={isSub ? `[CAR DE REMPLACEMENT] ${formattedOrigin}` : formattedOrigin} className="aff-classiques-arrivees__destination" />
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-arrivees__col-platform">
|
||||
@@ -1359,7 +1393,12 @@ export default function ClassiquesArrivees({ defilement = null } = {}){
|
||||
speed={50}
|
||||
text={
|
||||
<span className="aff-classiques-arrivees__stops-chain">
|
||||
{stopsList.map((stopName, sIdx) => (
|
||||
{isSub && (
|
||||
<span className="aff-classiques-arrivees__stop-item" style={{ color: '#ffd200', fontWeight: 'bold' }}>
|
||||
CAR DE REMPLACEMENT
|
||||
<span className="aff-classiques-arrivees__stop-bullet">•</span>
|
||||
</span>
|
||||
)}{stopsList.map((stopName, sIdx) => (
|
||||
<span key={sIdx} className="aff-classiques-arrivees__stop-item">
|
||||
{sIdx > 0 && <span className="aff-classiques-arrivees__stop-bullet">•</span>}
|
||||
<span className="aff-classiques-arrivees__stop-name">{stopName}</span>
|
||||
|
||||
@@ -335,6 +335,7 @@
|
||||
.aff-classiques-departs__col-platform {
|
||||
width: 96px;
|
||||
min-width: 96px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -434,3 +435,4 @@
|
||||
margin-top: 1px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
@@ -630,7 +630,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
}
|
||||
|
||||
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 };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
@@ -696,12 +701,17 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
let inFlight = false; // empêche les fetchs concurrents
|
||||
let pendingRefresh = false;
|
||||
const source = getSourceFromPath(window.location.pathname);
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!mounted) return;
|
||||
if (inFlight) return;
|
||||
if (inFlight) {
|
||||
pendingRefresh = true;
|
||||
return;
|
||||
}
|
||||
inFlight = true;
|
||||
pendingRefresh = false;
|
||||
|
||||
if (source === 'sncf') {
|
||||
setLoading(true);
|
||||
@@ -741,6 +751,10 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
} finally {
|
||||
if (mounted) setLoading(false);
|
||||
inFlight = false;
|
||||
if (pendingRefresh && mounted) {
|
||||
pendingRefresh = false;
|
||||
fetchData();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1169,8 +1183,27 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
const intervalMs = 15000; // 15 secondes par défaut
|
||||
const intervalId = setInterval(fetchData, intervalMs);
|
||||
|
||||
return () => { mounted = false; clearInterval(intervalId); };
|
||||
}, [gareDisplay, gareParam, gareIdParam, parseTimeHM, buildPerturbMap, computeStatusForHoraireAtStop, findPerturbsForHoraireAndStop, normalizeName, formatTimeFromHM, applyPerturbationsToHoraires, debug, getStops, getPerturbationJours, defilementMode]);
|
||||
let rtDebounce = null;
|
||||
const onRtUpdate = () => {
|
||||
if (!mounted) return;
|
||||
clearTimeout(rtDebounce);
|
||||
rtDebounce = setTimeout(() => {
|
||||
if (mounted) fetchData();
|
||||
}, 2000);
|
||||
};
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('sncf-realtime-updated', onRtUpdate);
|
||||
}
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
clearInterval(intervalId);
|
||||
clearTimeout(rtDebounce);
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('sncf-realtime-updated', onRtUpdate);
|
||||
}
|
||||
};
|
||||
}, [gareDisplay, gareParam, gareIdParam, defilementMode]);
|
||||
|
||||
// Helpers pour perturbations (memoized plus haut)
|
||||
|
||||
@@ -1253,7 +1286,7 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
}
|
||||
|
||||
const horaire = h || {};
|
||||
const { logoSrc, typeLabel, numTrain } = getTrainLogoAndMeta(horaire);
|
||||
const { logoSrc, typeLabel, numTrain, isCar: metaIsCar, isSubstitution: metaIsSubstitution, brand: metaBrand, brandClass } = getTrainLogoAndMeta(horaire, gareDisplay || gareParam);
|
||||
const time = horaire._displayTime || formatTimeNoSeconds(horaire.heure_depart || horaire.departure_time || horaire.time || horaire.heure || '');
|
||||
const destination = horaire.gare_arrivee_nom || horaire.gare_arrivee || horaire.destination || '';
|
||||
const formattedDestination = formatStationName(destination);
|
||||
@@ -1274,7 +1307,7 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
const status = horaire._status || computeStatusForHoraireAtStop(horaire, stationId, stationName, buildPerturbMap(perturbations));
|
||||
const isSuppressed = status && status.label === 'supprimé';
|
||||
const isDelayed = status && status.label && String(status.label).startsWith('retardé');
|
||||
const isCar = (
|
||||
const isCar = metaIsCar || (
|
||||
(horaire.type_train && (horaire.type_train.toUpperCase().includes('AUTOCAR') || horaire.type_train.toUpperCase().includes('CAR TER') || horaire.type_train.toUpperCase() === 'BUS')) ||
|
||||
(horaire.op && (horaire.op.toUpperCase().includes('AUTOCAR') || horaire.op.toUpperCase().includes('CAR TER') || horaire.op.toUpperCase() === 'BUS')) ||
|
||||
(horaire.type && (horaire.type.toUpperCase().includes('AUTOCAR') || horaire.type.toUpperCase().includes('CAR TER') || horaire.type.toUpperCase() === 'BUS')) ||
|
||||
@@ -1288,6 +1321,7 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
cleanPlatform === 'CAR' ||
|
||||
cleanPlatform === 'BUS'
|
||||
);
|
||||
const isSub = metaIsSubstitution || Boolean(horaire.is_substitution);
|
||||
const showPlatform = !isSuppressed && (isCar || Boolean(cleanPlatform));
|
||||
|
||||
const stopsList = isExtended ? getStopsListForDeparts(horaire, gareDisplay || gareParam, normalizeName) : [];
|
||||
@@ -1334,7 +1368,7 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-departs__col-destination">
|
||||
<MarqueeText text={formattedDestination} className="aff-classiques-departs__destination" />
|
||||
<MarqueeText text={isSub ? `[CAR DE REMPLACEMENT] ${formattedDestination}` : formattedDestination} className="aff-classiques-departs__destination" />
|
||||
</div>
|
||||
|
||||
<div className="aff-classiques-departs__col-platform">
|
||||
@@ -1358,7 +1392,12 @@ export default function ClassiquesDeparts({ defilement = null } = {}){
|
||||
speed={50}
|
||||
text={
|
||||
<span className="aff-classiques-departs__stops-chain">
|
||||
{stopsList.map((stopName, sIdx) => (
|
||||
{isSub && (
|
||||
<span className="aff-classiques-departs__stop-item" style={{ color: '#ffd200', fontWeight: 'bold' }}>
|
||||
CAR DE REMPLACEMENT
|
||||
<span className="aff-classiques-departs__stop-bullet">•</span>
|
||||
</span>
|
||||
)}{stopsList.map((stopName, sIdx) => (
|
||||
<span key={sIdx} className="aff-classiques-departs__stop-item">
|
||||
{sIdx > 0 && <span className="aff-classiques-departs__stop-bullet">•</span>}
|
||||
<span className="aff-classiques-departs__stop-name">{stopName}</span>
|
||||
|
||||
@@ -185,13 +185,25 @@
|
||||
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 {
|
||||
background-color: #ffa74f !important;
|
||||
color: #000c2f !important;
|
||||
}
|
||||
|
||||
.eva-arr-status-pill--suppressed {
|
||||
background-color: #ff5c73 !important;
|
||||
background-color: #ff4d6d !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
@@ -242,20 +254,28 @@
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
/* Badge Motif de Perturbation (ex: Embouteillages) */
|
||||
/* Badge Motif de Perturbation */
|
||||
.eva-arr-perturb-badge {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
background-color: #ffa74f !important;
|
||||
color: #000c2f !important;
|
||||
padding: 4px 15px !important;
|
||||
border-radius: 6px !important;
|
||||
height: 37px !important;
|
||||
height: 38px !important;
|
||||
box-sizing: border-box !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 {
|
||||
width: 26px !important;
|
||||
height: 26px !important;
|
||||
@@ -263,12 +283,23 @@
|
||||
}
|
||||
|
||||
.eva-arr-perturb-badge-text {
|
||||
font-size: 24px !important;
|
||||
font-size: 22px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 1 !important;
|
||||
line-height: 1.1 !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 */
|
||||
.eva-arr-vias-container {
|
||||
display: flex !important;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import './EVAArrivees.css';
|
||||
import supabase from '../../../lib/supabase';
|
||||
@@ -178,6 +179,41 @@ const EVACrossIcon = ({ size = '27px', color = 'currentColor', style = {} }) =>
|
||||
</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)
|
||||
const EVAViasMarquee = ({ viaNames }) => {
|
||||
const containerRef = useRef(null);
|
||||
@@ -206,7 +242,7 @@ const EVAViasMarquee = ({ viaNames }) => {
|
||||
if (!viaNames || viaNames.length === 0) {
|
||||
return (
|
||||
<div className="eva-arr-vias-container">
|
||||
<span className="eva-arr-vias-stop">Direct</span>
|
||||
<span className="eva-arr-vias-direct">train direct</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -222,12 +258,24 @@ const EVAViasMarquee = ({ viaNames }) => {
|
||||
animationDuration: isOverflowing ? `${duration}s` : undefined
|
||||
}}
|
||||
>
|
||||
{viaNames.map((v, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <span className="eva-arr-vias-bullet">•</span>}
|
||||
<span className="eva-arr-vias-stop">{v}</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{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}>
|
||||
{i > 0 && <span className="eva-arr-vias-bullet">•</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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -809,7 +857,12 @@ export default function EVAArrivees() {
|
||||
}
|
||||
|
||||
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 };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
@@ -1230,7 +1283,7 @@ export default function EVAArrivees() {
|
||||
fetchData();
|
||||
const intervalId = setInterval(fetchData, 15000);
|
||||
return () => { mounted = false; clearInterval(intervalId); };
|
||||
}, [gareDisplay, gareParam, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]);
|
||||
}, [gareDisplay, gareParam, gareIdParam]);
|
||||
|
||||
// Alternance retard / texte toutes les 2s
|
||||
const [alternateDelay, setAlternateDelay] = useState(false);
|
||||
@@ -1309,6 +1362,15 @@ export default function EVAArrivees() {
|
||||
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 (
|
||||
<div className="eva-arr-container">
|
||||
{/* HEADER */}
|
||||
@@ -1316,9 +1378,9 @@ export default function EVAArrivees() {
|
||||
<div className="eva-arr-header-left">
|
||||
<EVAArrivalsHeaderIcon className="eva-arr-header-icon" />
|
||||
<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-title-sub">Arrivals From / Ankünfte Aus</span>
|
||||
<span className="eva-arr-header-title-sub">ARRIVALS FROM / LLEGADAS DE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1350,7 +1412,7 @@ export default function EVAArrivees() {
|
||||
horaire.voie ||
|
||||
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 isCarTER = (op === 'CAR TER' || op === 'AUTOCAR');
|
||||
@@ -1367,19 +1429,42 @@ export default function EVAArrivees() {
|
||||
|
||||
let delayVal = '+10 min';
|
||||
if (typeof status?.delayMinutes === 'number' && status.delayMinutes > 0) {
|
||||
delayVal = `+${status.delayMinutes} min`;
|
||||
delayVal = `+ ${status.delayMinutes} min`;
|
||||
} else if (status?.delayMinutes) {
|
||||
delayVal = `+${status.delayMinutes} min`;
|
||||
delayVal = `+ ${status.delayMinutes} min`;
|
||||
} else if (status?.label && status.label.includes('+')) {
|
||||
const m = status.label.match(/\+(\d+)\s*(?:min)?/);
|
||||
if (m) delayVal = `+${m[1]} min`;
|
||||
if (m) delayVal = `+ ${m[1]} min`;
|
||||
else delayVal = status.label;
|
||||
}
|
||||
|
||||
let pillText = isSuppressed ? 'supprimé' : isDelayed ? (alternateDelay ? 'retardé' : delayVal) : 'à l\'heure';
|
||||
const pillModifier = isSuppressed ? 'suppressed' : isDelayed ? 'delayed' : 'ontime';
|
||||
const timeHM = horaire._arrHM || 0;
|
||||
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' : ''));
|
||||
|
||||
// Calcul des gares intermédiaires (Vias)
|
||||
@@ -1392,7 +1477,8 @@ export default function EVAArrivees() {
|
||||
const norm = normalizeName(sName);
|
||||
if (norm === normalizeName(origin || '')) continue;
|
||||
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 +1490,11 @@ export default function EVAArrivees() {
|
||||
<div className="eva-arr-col-time">
|
||||
<div className="eva-arr-time-value">{time}</div>
|
||||
<div className={`eva-arr-status-pill eva-arr-status-pill--${pillModifier}`}>
|
||||
{isSuppressed ? (
|
||||
<EVACrossIcon className="eva-arr-status-icon" color="#ffffff" />
|
||||
) : (
|
||||
<EVAClockIcon className="eva-arr-status-icon" color={isDelayed ? '#000c2f' : '#1a2b4c'} />
|
||||
{showClockIcon && (
|
||||
<EVAClockIcon
|
||||
className="eva-arr-status-icon"
|
||||
color={isSuppressed ? '#ffffff' : isDelayed ? '#000c2f' : '#1a2b4c'}
|
||||
/>
|
||||
)}
|
||||
<span className="eva-arr-status-text">{pillText}</span>
|
||||
</div>
|
||||
@@ -1418,10 +1505,13 @@ export default function EVAArrivees() {
|
||||
<div className="eva-arr-origin-row">
|
||||
<MarqueeText text={decodedOrigin} className="eva-arr-origin-name" style={{ flex: 'none' }} />
|
||||
{perturbationReason && (
|
||||
<div className="eva-arr-perturb-badge">
|
||||
<EVAInfoIcon className="eva-arr-perturb-badge-icon" />
|
||||
<span className="eva-arr-perturb-badge-text">{perturbationReason}</span>
|
||||
</div>
|
||||
alternateDelay ? (
|
||||
<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>
|
||||
</div>
|
||||
) : (
|
||||
isSuppressed ? <EVAPerturbWarningSquare /> : <EVAPerturbInfoSquare />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<EVAViasMarquee viaNames={viaNames} />
|
||||
@@ -1429,11 +1519,15 @@ export default function EVAArrivees() {
|
||||
|
||||
{/* SECTION DROITE : MODE / VOIE & BLOC OPERATEUR/NUMERO */}
|
||||
<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">
|
||||
<EVABusIcon className="eva-arr-bus-icon" />
|
||||
</div>
|
||||
) : (rawPlatform && !isSuppressed) ? (
|
||||
) : rawPlatform ? (
|
||||
<div className="eva-arr-voie-box">
|
||||
<span className="eva-arr-voie-label">Voie</span>
|
||||
<span className="eva-arr-voie-val">{rawPlatform}</span>
|
||||
@@ -1442,7 +1536,7 @@ export default function EVAArrivees() {
|
||||
|
||||
<div className="eva-arr-train-block">
|
||||
<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>
|
||||
</li>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import './EVADeparts.css';
|
||||
import supabase from '../../../lib/supabase';
|
||||
@@ -805,7 +806,12 @@ export default function EVADeparts() {
|
||||
}
|
||||
|
||||
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 };
|
||||
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
|
||||
|
||||
@@ -1243,7 +1249,7 @@ export default function EVADeparts() {
|
||||
fetchData();
|
||||
const intervalId = setInterval(fetchData, 15000);
|
||||
return () => { mounted = false; clearInterval(intervalId); };
|
||||
}, [gareDisplay, gareParam, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]);
|
||||
}, [gareDisplay, gareParam, gareIdParam]);
|
||||
|
||||
// Alternance retard / texte toutes les 2s
|
||||
const [alternateDelay, setAlternateDelay] = useState(false);
|
||||
|
||||
+350
-297
@@ -1,3 +1,81 @@
|
||||
/* eslint-disable */
|
||||
let isRealtimeRefreshing = false;
|
||||
|
||||
// Fonctions de persistance du cache temps réel dans localStorage et sessionStorage
|
||||
function saveRealtimeCacheToStorage() {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
if (siriLiteCache.platforms && siriLiteCache.platforms.size > 0) {
|
||||
const arr = Array.from(siriLiteCache.platforms.entries()).slice(0, 4000);
|
||||
const payload = JSON.stringify({
|
||||
ts: siriLiteCache.timestamp,
|
||||
data: arr
|
||||
});
|
||||
if (window.localStorage) {
|
||||
window.localStorage.setItem('sncf_siri_platforms', payload);
|
||||
}
|
||||
if (window.sessionStorage) {
|
||||
window.sessionStorage.setItem('sncf_siri_platforms', payload);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function restoreRealtimeCacheFromStorage() {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
const raw = (window.localStorage && window.localStorage.getItem('sncf_siri_platforms')) ||
|
||||
(window.sessionStorage && window.sessionStorage.getItem('sncf_siri_platforms'));
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && Array.isArray(parsed.data) && (Date.now() - parsed.ts < 1800000)) {
|
||||
siriLiteCache.timestamp = parsed.ts;
|
||||
siriLiteCache.platforms = new Map(parsed.data);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
let lastRealtimeTriggerTime = 0;
|
||||
|
||||
let activeRealtimePromise = null;
|
||||
|
||||
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;
|
||||
|
||||
activeRealtimePromise = (async () => {
|
||||
isRealtimeRefreshing = true;
|
||||
try {
|
||||
const promises = [];
|
||||
if (needRt) promises.push(fetchGtfsRealtimeData());
|
||||
if (needSiri) promises.push(fetchSiriLiteData());
|
||||
if (needSx) promises.push(fetchSituationExchangeData());
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
} catch (_) {
|
||||
// ignore
|
||||
} finally {
|
||||
isRealtimeRefreshing = false;
|
||||
activeRealtimePromise = null;
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('sncf-realtime-updated'));
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return activeRealtimePromise;
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
// Service d'accès aux données SNCF (GTFS / GTFS-RT / SIRI Lite / Open Data SNCF)
|
||||
import { transit_realtime } from 'gtfs-realtime-bindings';
|
||||
import { findGtfsGare, searchGtfsGares } from './gtfsStations';
|
||||
@@ -8,8 +86,15 @@ const SNCF_SIRI_LITE_TIMETABLE = 'https://proxy.transport.data.gouv.fr/resource/
|
||||
const SNCF_SIRI_LITE_SITUATION_EXCHANGE = 'https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-situation-exchange';
|
||||
const NAVITIA_TOKEN = (typeof process !== 'undefined' && process.env && process.env.REACT_APP_NAVITIA_TOKEN) || 'bf06df97-2ff1-4560-b74c-f127cb70764d';
|
||||
|
||||
// TTL de rafraîchissement des flux temps réel SNCF : 2 minutes (120 secondes)
|
||||
const REALTIME_CACHE_TTL = 120000;
|
||||
// Détection du mode développement pour accélération maximale
|
||||
const isDev = (typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'development') ||
|
||||
(typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'));
|
||||
|
||||
// TTL de rafraîchissement des flux temps réel SNCF : 5 minutes en dev, 2 minutes en prod
|
||||
const REALTIME_CACHE_TTL = isDev ? 300000 : 120000;
|
||||
|
||||
// Cache mémoire des données statiques des gares (chargement instantané en dev)
|
||||
const stationJsonCache = new Map();
|
||||
|
||||
// Cache mémoire pour le flux GTFS-RT (TTL = 2 min)
|
||||
let gtfsRtCache = {
|
||||
@@ -34,7 +119,7 @@ let situationExchangeCache = {
|
||||
};
|
||||
|
||||
// Cache mémoire pour l'API OpenData SNCF (TTL = 2 min)
|
||||
const openDataPlatformsCache = new Map();
|
||||
restoreRealtimeCacheFromStorage();
|
||||
|
||||
const MAJOR_HUBS = [
|
||||
'paris', 'lyon', 'dijon', 'marseille', 'lille', 'strasbourg', 'bordeaux',
|
||||
@@ -78,7 +163,7 @@ export function resolveNominalTrack(stationName, trainNum, uic = '') {
|
||||
}
|
||||
|
||||
// Helper fetch avec timeout pour ne jamais bloquer l'affichage en cas de réseau lent ou de blocage
|
||||
async function fetchWithTimeout(url, options = {}, timeoutMs = 25000) {
|
||||
async function fetchWithTimeout(url, options = {}, timeoutMs = 15000) {
|
||||
const controller = new AbortController();
|
||||
const id = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
@@ -137,9 +222,124 @@ function extractAllTrainNumbers(journeyXml) {
|
||||
return Array.from(nums);
|
||||
}
|
||||
|
||||
|
||||
function processJourneyXml(j, platformsMap, updatesMap) {
|
||||
// 1. Extraction exhaustive des identifiants et numéros de train
|
||||
const trainNums = extractAllTrainNumbers(j);
|
||||
|
||||
const journeyRefMatch = j.match(/<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/) ||
|
||||
j.match(/<VehicleJourneyRef>([^<]+)<\/VehicleJourneyRef>/) ||
|
||||
j.match(/<FramedVehicleJourneyRef>[\s\S]*?<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/);
|
||||
const journeyRef = journeyRefMatch ? journeyRefMatch[1].trim() : '';
|
||||
|
||||
const isCanceledJourney = j.includes('<Cancellation>true</Cancellation>') || j.includes('<JourneyStatus>cancelled</JourneyStatus>');
|
||||
|
||||
// 2. Extraire tous les arrêts desservis (RecordedCall et EstimatedCall)
|
||||
const callMatches = j.match(/<(?:RecordedCall|EstimatedCall)>[\s\S]*?<\/(?:RecordedCall|EstimatedCall)>/g) || [];
|
||||
for (const call of callMatches) {
|
||||
// Extraction du code UIC de l'arrêt
|
||||
const stopRefMatch = call.match(/<StopPointRef>([^<]+)<\/StopPointRef>/);
|
||||
let uic = '';
|
||||
let uic7 = '';
|
||||
if (stopRefMatch) {
|
||||
const rawRef = stopRefMatch[1];
|
||||
const uicDigits = rawRef.match(/\d{7,8}/);
|
||||
uic = uicDigits ? uicDigits[0] : rawRef.replace(/\D/g, '');
|
||||
if (uic.length === 8 && uic.startsWith('87')) {
|
||||
uic7 = uic.slice(2);
|
||||
}
|
||||
}
|
||||
|
||||
const stopNameMatch = call.match(/<StopPointName>([^<]+)<\/StopPointName>/);
|
||||
const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
// Extraction des quais (Départ, Arrivée, Prévu ou Réel)
|
||||
const depPlatMatch = call.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName)>([^<]+)<\/(?:DeparturePlatformName|PlannedDeparturePlatformName)>/);
|
||||
const arrPlatMatch = call.match(/<(?:ArrivalPlatformName|PlannedArrivalPlatformName)>([^<]+)<\/(?:ArrivalPlatformName|PlannedArrivalPlatformName)>/);
|
||||
const quayMatch = call.match(/<QuayRef>([^<]+)<\/QuayRef>/);
|
||||
|
||||
let rawPlatform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : (quayMatch ? quayMatch[1] : ''))).trim();
|
||||
const platform = rawPlatform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
|
||||
// Extraction des heures et calcul du retard
|
||||
const expDepMatch = call.match(/<ExpectedDepartureTime>([^<]+)<\/ExpectedDepartureTime>/);
|
||||
const aimDepMatch = call.match(/<AimedDepartureTime>([^<]+)<\/AimedDepartureTime>/);
|
||||
const expArrMatch = call.match(/<ExpectedArrivalTime>([^<]+)<\/ExpectedArrivalTime>/);
|
||||
const aimArrMatch = call.match(/<AimedArrivalTime>([^<]+)<\/AimedArrivalTime>/);
|
||||
|
||||
let delayMin = 0;
|
||||
const expTimeStr = (expDepMatch && expDepMatch[1]) || (expArrMatch && expArrMatch[1]);
|
||||
const aimTimeStr = (aimDepMatch && aimDepMatch[1]) || (aimArrMatch && aimArrMatch[1]);
|
||||
|
||||
if (expTimeStr && aimTimeStr) {
|
||||
const expD = new Date(expTimeStr).getTime();
|
||||
const aimD = new Date(aimTimeStr).getTime();
|
||||
if (!isNaN(expD) && !isNaN(aimD)) {
|
||||
delayMin = Math.max(0, Math.round((expD - aimD) / 60000));
|
||||
}
|
||||
}
|
||||
|
||||
const isStopCancelled = isCanceledJourney || call.includes('<DepartureStatus>cancelled</DepartureStatus>') || call.includes('<ArrivalStatus>cancelled</ArrivalStatus>') || call.includes('<Cancellation>true</Cancellation>');
|
||||
|
||||
if (platform) {
|
||||
for (const tNum of trainNums) {
|
||||
platformsMap.set(tNum, platform);
|
||||
const ct = cleanNum(tNum);
|
||||
if (ct) platformsMap.set(ct, platform);
|
||||
if (uic) {
|
||||
platformsMap.set(`${tNum}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${tNum}`, platform);
|
||||
if (ct) {
|
||||
platformsMap.set(`${ct}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${ct}`, platform);
|
||||
}
|
||||
}
|
||||
if (uic7) {
|
||||
platformsMap.set(`${tNum}_${uic7}`, platform);
|
||||
platformsMap.set(`${uic7}_${tNum}`, platform);
|
||||
if (ct) {
|
||||
platformsMap.set(`${ct}_${uic7}`, platform);
|
||||
platformsMap.set(`${uic7}_${ct}`, platform);
|
||||
}
|
||||
}
|
||||
if (stopNameClean) {
|
||||
platformsMap.set(`${tNum}_${stopNameClean}`, platform);
|
||||
if (ct) platformsMap.set(`${ct}_${stopNameClean}`, platform);
|
||||
}
|
||||
}
|
||||
if (journeyRef) {
|
||||
platformsMap.set(journeyRef, platform);
|
||||
if (uic) {
|
||||
platformsMap.set(`${journeyRef}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${journeyRef}`, platform);
|
||||
}
|
||||
if (stopNameClean) {
|
||||
platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const info = {
|
||||
platform: platform,
|
||||
delayMinutes: delayMin,
|
||||
isCanceled: isStopCancelled
|
||||
};
|
||||
|
||||
for (const tNum of trainNums) {
|
||||
if (uic) updatesMap.set(`${tNum}_${uic}`, info);
|
||||
updatesMap.set(tNum, info);
|
||||
}
|
||||
if (journeyRef) {
|
||||
if (uic) updatesMap.set(`${journeyRef}_${uic}`, info);
|
||||
updatesMap.set(journeyRef, info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
@@ -147,128 +347,83 @@ async function fetchSiriLiteData() {
|
||||
return siriLiteCache;
|
||||
}
|
||||
|
||||
const platformsMap = new Map();
|
||||
const updatesMap = new Map();
|
||||
// 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, */*'
|
||||
}
|
||||
}, 25000);
|
||||
}, 45000);
|
||||
|
||||
if (res && res.ok) {
|
||||
const xml = await res.text();
|
||||
const journeys = xml.split('<EstimatedVehicleJourney>');
|
||||
let lastDispatchTime = Date.now();
|
||||
let newPlatformsCount = 0;
|
||||
|
||||
for (let i = 1; i < journeys.length; i++) {
|
||||
const j = journeys[i];
|
||||
if (res.body && typeof res.body.getReader === 'function') {
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
// 1. Extraction exhaustive des identifiants et numéros de train
|
||||
const trainNums = extractAllTrainNumbers(j);
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const journeyRefMatch = j.match(/<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/) || j.match(/<VehicleJourneyRef>([^<]+)<\/VehicleJourneyRef>/) || j.match(/<FramedVehicleJourneyRef>[\s\S]*?<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/);
|
||||
const journeyRef = journeyRefMatch ? journeyRefMatch[1].trim() : '';
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
const isCanceledJourney = j.includes('<Cancellation>true</Cancellation>') || j.includes('<JourneyStatus>cancelled</JourneyStatus>');
|
||||
|
||||
// 2. Extraire tous les arrêts desservis (RecordedCall et EstimatedCall)
|
||||
const callMatches = j.match(/<(?:RecordedCall|EstimatedCall)>[\s\S]*?<\/(?:RecordedCall|EstimatedCall)>/g) || [];
|
||||
for (const call of callMatches) {
|
||||
// Extraction du code UIC de l'arrêt
|
||||
const stopRefMatch = call.match(/<StopPointRef>([^<]+)<\/StopPointRef>/);
|
||||
let uic = '';
|
||||
let uic7 = '';
|
||||
if (stopRefMatch) {
|
||||
const rawRef = stopRefMatch[1];
|
||||
const uicDigits = rawRef.match(/\d{7,8}/);
|
||||
uic = uicDigits ? uicDigits[0] : rawRef.replace(/\D/g, '');
|
||||
if (uic.length === 8 && uic.startsWith('87')) {
|
||||
uic7 = uic.slice(2);
|
||||
}
|
||||
}
|
||||
|
||||
const stopNameMatch = call.match(/<StopPointName>([^<]+)<\/StopPointName>/);
|
||||
const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
// Extraction des quais (Départ, Arrivée, Prévu ou Réel)
|
||||
const depPlatMatch = call.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName)>([^<]+)<\/(?:DeparturePlatformName|PlannedDeparturePlatformName)>/);
|
||||
const arrPlatMatch = call.match(/<(?:ArrivalPlatformName|PlannedArrivalPlatformName)>([^<]+)<\/(?:ArrivalPlatformName|PlannedArrivalPlatformName)>/);
|
||||
const quayMatch = call.match(/<QuayRef>([^<]+)<\/QuayRef>/);
|
||||
|
||||
let rawPlatform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : (quayMatch ? quayMatch[1] : ''))).trim();
|
||||
const platform = rawPlatform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
|
||||
// Extraction des heures et calcul du retard
|
||||
const expDepMatch = call.match(/<ExpectedDepartureTime>([^<]+)<\/ExpectedDepartureTime>/);
|
||||
const aimDepMatch = call.match(/<AimedDepartureTime>([^<]+)<\/AimedDepartureTime>/);
|
||||
const expArrMatch = call.match(/<ExpectedArrivalTime>([^<]+)<\/ExpectedArrivalTime>/);
|
||||
const aimArrMatch = call.match(/<AimedArrivalTime>([^<]+)<\/AimedArrivalTime>/);
|
||||
|
||||
let delayMin = 0;
|
||||
const expTimeStr = (expDepMatch && expDepMatch[1]) || (expArrMatch && expArrMatch[1]);
|
||||
const aimTimeStr = (aimDepMatch && aimDepMatch[1]) || (aimArrMatch && aimArrMatch[1]);
|
||||
|
||||
if (expTimeStr && aimTimeStr) {
|
||||
const expD = new Date(expTimeStr).getTime();
|
||||
const aimD = new Date(aimTimeStr).getTime();
|
||||
if (!isNaN(expD) && !isNaN(aimD)) {
|
||||
delayMin = Math.max(0, Math.round((expD - aimD) / 60000));
|
||||
}
|
||||
}
|
||||
|
||||
const isStopCancelled = isCanceledJourney || call.includes('<DepartureStatus>cancelled</DepartureStatus>') || call.includes('<ArrivalStatus>cancelled</ArrivalStatus>') || call.includes('<Cancellation>true</Cancellation>');
|
||||
|
||||
if (platform) {
|
||||
for (const tNum of trainNums) {
|
||||
if (uic) {
|
||||
platformsMap.set(`${tNum}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${tNum}`, platform);
|
||||
}
|
||||
if (uic7) {
|
||||
platformsMap.set(`${tNum}_${uic7}`, platform);
|
||||
platformsMap.set(`${uic7}_${tNum}`, platform);
|
||||
}
|
||||
if (stopNameClean) {
|
||||
platformsMap.set(`${tNum}_${stopNameClean}`, platform);
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (journeyRef) {
|
||||
if (uic) {
|
||||
platformsMap.set(`${journeyRef}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${journeyRef}`, platform);
|
||||
}
|
||||
if (stopNameClean) {
|
||||
platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
const info = {
|
||||
platform: platform,
|
||||
delayMinutes: delayMin,
|
||||
isCanceled: isStopCancelled
|
||||
};
|
||||
|
||||
for (const tNum of trainNums) {
|
||||
if (uic) updatesMap.set(`${tNum}_${uic}`, info);
|
||||
updatesMap.set(tNum, info);
|
||||
}
|
||||
if (journeyRef) {
|
||||
if (uic) updatesMap.set(`${journeyRef}_${uic}`, info);
|
||||
updatesMap.set(journeyRef, info);
|
||||
}
|
||||
if (platformsMap.size > 0 || updatesMap.size > 0) {
|
||||
siriLiteCache = {
|
||||
timestamp: Date.now(),
|
||||
platforms: platformsMap,
|
||||
updates: updatesMap
|
||||
};
|
||||
saveRealtimeCacheToStorage();
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('sncf-realtime-updated'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
siriLiteCache = {
|
||||
timestamp: now,
|
||||
platforms: platformsMap,
|
||||
updates: updatesMap
|
||||
};
|
||||
} 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;
|
||||
@@ -293,7 +448,7 @@ async function fetchSituationExchangeData() {
|
||||
headers: {
|
||||
'Accept': 'application/xml, text/xml, */*'
|
||||
}
|
||||
}, 12000);
|
||||
}, 20000);
|
||||
|
||||
if (res && res.ok) {
|
||||
const xml = await res.text();
|
||||
@@ -387,12 +542,14 @@ async function fetchSituationExchangeData() {
|
||||
}
|
||||
}
|
||||
|
||||
situationExchangeCache = {
|
||||
timestamp: now,
|
||||
platforms: platformsMap,
|
||||
updates: updatesMap,
|
||||
alerts: alertsList
|
||||
};
|
||||
if (platformsMap.size > 0 || updatesMap.size > 0 || alertsList.length > 0) {
|
||||
situationExchangeCache = {
|
||||
timestamp: now,
|
||||
platforms: platformsMap,
|
||||
updates: updatesMap,
|
||||
alerts: alertsList
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Situation Exchange error/timeout:', err);
|
||||
}
|
||||
@@ -404,153 +561,11 @@ async function fetchSituationExchangeData() {
|
||||
* Récupère les quais en temps réel depuis l'API OpenData SNCF (Navitia + OpenDataSoft)
|
||||
*/
|
||||
async function fetchOpenDataSncfPlatforms(uic, mode = 'departs') {
|
||||
if (!uic) return new Map();
|
||||
const cacheKey = `${uic}_${mode}`;
|
||||
const now = Date.now();
|
||||
if (openDataPlatformsCache.has(cacheKey)) {
|
||||
const cached = openDataPlatformsCache.get(cacheKey);
|
||||
if (now - cached.timestamp < 30000) {
|
||||
return cached.platforms;
|
||||
}
|
||||
}
|
||||
|
||||
const platformsMap = new Map();
|
||||
|
||||
try {
|
||||
const authHeader = NAVITIA_TOKEN.startsWith('Basic') ? NAVITIA_TOKEN : 'Basic ' + btoa(NAVITIA_TOKEN + ':');
|
||||
const uicClean = String(uic).replace(/\D/g, '');
|
||||
const isArrival = mode === 'arrivees';
|
||||
const navitiaPath = isArrival ? 'arrivals' : 'departures';
|
||||
const odsDataset = isArrival ? 'prochaines-arrivees-des-trains-en-gare-temps-reel' : 'prochains-departs-des-trains-en-gare-temps-reel';
|
||||
|
||||
const navitiaEndpoints = [
|
||||
`https://api.sncf.com/v1/coverage/sncf/stop_areas/stop_area:OCE:SA:${uicClean}/${navitiaPath}?count=100`,
|
||||
`https://api.sncf.com/v1/coverage/sncf/stop_areas/stop_area:SNCF:${uicClean}/${navitiaPath}?count=100`,
|
||||
`https://api.sncf.com/v1/coverage/sncf/stop_points/stop_point:OCE:SP:${uicClean}/${navitiaPath}?count=100`,
|
||||
`https://api.navitia.io/v1/coverage/sncf/stop_areas/stop_area:OCE:SA:${uicClean}/${navitiaPath}?count=100`,
|
||||
`https://api.navitia.io/v1/coverage/sncf/stop_areas/stop_area:${uicClean}/${navitiaPath}?count=100`
|
||||
];
|
||||
|
||||
const [navitiaRes, odsV2Res, odsV1Res] = await Promise.allSettled([
|
||||
fetchWithTimeout(navitiaEndpoints[0], { headers: { 'Authorization': authHeader } }, 5000)
|
||||
.then(async res => {
|
||||
if (res && res.ok) return res;
|
||||
return fetchWithTimeout(navitiaEndpoints[1], { headers: { 'Authorization': authHeader } }, 5000);
|
||||
})
|
||||
.then(async res => {
|
||||
if (res && res.ok) return res;
|
||||
return fetchWithTimeout(navitiaEndpoints[3], { headers: { 'Authorization': NAVITIA_TOKEN } }, 5000);
|
||||
}),
|
||||
fetchWithTimeout(`https://ressources.data.sncf.com/api/explore/v2.1/catalog/datasets/${odsDataset}/records?limit=100&where=search(code_uic%2C%22${uicClean}%22)`, {}, 5000),
|
||||
fetchWithTimeout(`https://data.sncf.com/api/records/1.0/search/?dataset=${odsDataset}&q=${uicClean}&rows=100`, {}, 5000)
|
||||
]);
|
||||
|
||||
// 1. Décodage Navitia (API OpenData SNCF temps réel)
|
||||
if (navitiaRes.status === 'fulfilled' && navitiaRes.value && navitiaRes.value.ok) {
|
||||
const data = await navitiaRes.value.json();
|
||||
const list = (isArrival ? data.arrivals : data.departures) || [];
|
||||
for (const item of list) {
|
||||
const info = item.display_informations || {};
|
||||
const trainNum = cleanNum(info.headsign || info.trip_short_name || info.commercial_mode || info.code || '');
|
||||
const rawNum = String(info.headsign || info.trip_short_name || info.code || '').replace(/\D/g, '');
|
||||
|
||||
const stopPoint = item.stop_point || (item.stop_date_time && item.stop_date_time.stop_point) || {};
|
||||
let platform = (
|
||||
stopPoint.platform_code ||
|
||||
info.platform ||
|
||||
''
|
||||
).toString().trim();
|
||||
|
||||
if (!platform && stopPoint.label) {
|
||||
const m = stopPoint.label.match(/(?:voie|quai|track|platform)\s*([a-z0-9]+)/i);
|
||||
if (m) platform = m[1];
|
||||
}
|
||||
|
||||
platform = platform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
|
||||
const destClean = info.direction ? formatStationName(info.direction).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
if (platform) {
|
||||
if (trainNum && uicClean) {
|
||||
platformsMap.set(`${trainNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${trainNum}`, platform);
|
||||
}
|
||||
if (rawNum && uicClean) {
|
||||
platformsMap.set(`${rawNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${rawNum}`, platform);
|
||||
}
|
||||
if (trainNum && destClean) {
|
||||
platformsMap.set(`${trainNum}_${destClean}`, platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Décodage OpenDataSoft SNCF v2.1
|
||||
if (odsV2Res.status === 'fulfilled' && odsV2Res.value && odsV2Res.value.ok) {
|
||||
const data = await odsV2Res.value.json();
|
||||
const records = data.results || data.records || [];
|
||||
for (const r of records) {
|
||||
const fields = r.fields || r;
|
||||
const trainNum = cleanNum(fields.num || fields.numero || fields.train || fields.num_train || '');
|
||||
const rawNum = String(fields.num || fields.numero || fields.train || fields.num_train || '').replace(/\D/g, '');
|
||||
const platform = String(fields.voie || fields.quai || fields.platform || fields.track || fields.voie_quai || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
const destClean = fields.destination || fields.orig ? formatStationName(fields.destination || fields.orig).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
if (platform) {
|
||||
if (trainNum && uicClean) {
|
||||
platformsMap.set(`${trainNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${trainNum}`, platform);
|
||||
}
|
||||
if (rawNum && uicClean) {
|
||||
platformsMap.set(`${rawNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${rawNum}`, platform);
|
||||
}
|
||||
if (trainNum && destClean) {
|
||||
platformsMap.set(`${trainNum}_${destClean}`, platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Décodage OpenDataSoft SNCF v1.0
|
||||
if (odsV1Res.status === 'fulfilled' && odsV1Res.value && odsV1Res.value.ok) {
|
||||
const data = await odsV1Res.value.json();
|
||||
const records = data.records || [];
|
||||
for (const r of records) {
|
||||
const fields = r.fields || {};
|
||||
const trainNum = cleanNum(fields.num || fields.numero || fields.train || fields.num_train || '');
|
||||
const rawNum = String(fields.num || fields.numero || fields.train || fields.num_train || '').replace(/\D/g, '');
|
||||
const platform = String(fields.voie || fields.quai || fields.platform || fields.track || fields.voie_quai || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
|
||||
const destClean = fields.destination || fields.orig ? formatStationName(fields.destination || fields.orig).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
if (platform) {
|
||||
if (trainNum && uicClean) {
|
||||
platformsMap.set(`${trainNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${trainNum}`, platform);
|
||||
}
|
||||
if (rawNum && uicClean) {
|
||||
platformsMap.set(`${rawNum}_${uicClean}`, platform);
|
||||
platformsMap.set(`${uicClean}_${rawNum}`, platform);
|
||||
}
|
||||
if (trainNum && destClean) {
|
||||
platformsMap.set(`${trainNum}_${destClean}`, platform);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Notice: OpenData SNCF platform fetch error:', err);
|
||||
}
|
||||
|
||||
openDataPlatformsCache.set(cacheKey, {
|
||||
timestamp: now,
|
||||
platforms: platformsMap
|
||||
});
|
||||
|
||||
return platformsMap;
|
||||
// L'API api.sncf.com v1 et data.sncf.com (v1) sont obsolètes et retournent des erreurs 401/404.
|
||||
// Les tokens gratuits Navitia ne fonctionnent souvent plus pour la couverture SNCF globale.
|
||||
// On retourne une Map vide et on laisse SIRI-Lite gérer les quais en temps réel.
|
||||
return new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère et décode les mises à jour temps réel GTFS-RT SNCF
|
||||
*/
|
||||
@@ -565,8 +580,8 @@ async function fetchGtfsRealtimeData() {
|
||||
|
||||
try {
|
||||
const [rtRes, alertsRes] = await Promise.allSettled([
|
||||
fetchWithTimeout(SNCF_GTFS_RT_TRIP_UPDATES, {}, 6000),
|
||||
fetchWithTimeout(SNCF_GTFS_RT_SERVICE_ALERTS, {}, 6000)
|
||||
fetchWithTimeout(SNCF_GTFS_RT_TRIP_UPDATES, {}, 15000),
|
||||
fetchWithTimeout(SNCF_GTFS_RT_SERVICE_ALERTS, {}, 15000)
|
||||
]);
|
||||
|
||||
// 1. Décodage Trip Updates (Retards, Suppressions)
|
||||
@@ -603,8 +618,26 @@ async function fetchGtfsRealtimeData() {
|
||||
stopUpdates: stopUpdates
|
||||
};
|
||||
|
||||
const cTrainNo = cleanNum(trainNo);
|
||||
if (tid) updatesMap.set(tid, updateObj);
|
||||
if (trainNo) updatesMap.set(trainNo, updateObj);
|
||||
if (cTrainNo) updatesMap.set(cTrainNo, updateObj);
|
||||
|
||||
stopUpdates.forEach(stu => {
|
||||
const stopId = String(stu.stopId || '').replace(/\D/g, '');
|
||||
if (stopId) {
|
||||
const stopDelay = stu.departure && typeof stu.departure.delay === 'number'
|
||||
? stu.departure.delay
|
||||
: (stu.arrival && typeof stu.arrival.delay === 'number' ? stu.arrival.delay : maxDelaySec);
|
||||
const stopDelayMin = Math.max(0, Math.round(stopDelay / 60));
|
||||
const stopUpdateObj = {
|
||||
...updateObj,
|
||||
delayMinutes: stopDelayMin
|
||||
};
|
||||
if (trainNo) updatesMap.set(`${trainNo}_${stopId}`, stopUpdateObj);
|
||||
if (cTrainNo) updatesMap.set(`${cTrainNo}_${stopId}`, stopUpdateObj);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -630,11 +663,13 @@ async function fetchGtfsRealtimeData() {
|
||||
});
|
||||
}
|
||||
|
||||
gtfsRtCache = {
|
||||
timestamp: now,
|
||||
updates: updatesMap,
|
||||
alerts: alertsList
|
||||
};
|
||||
if (updatesMap.size > 0 || alertsList.length > 0) {
|
||||
gtfsRtCache = {
|
||||
timestamp: now,
|
||||
updates: updatesMap,
|
||||
alerts: alertsList
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Notice: GTFS-RT stream refresh error:', err);
|
||||
}
|
||||
@@ -719,36 +754,35 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
const publicUrl = (typeof process !== 'undefined' && process.env && process.env.PUBLIC_URL) ? process.env.PUBLIC_URL : '';
|
||||
const stationUrl = `${publicUrl}/data/stations/${uic}.json`;
|
||||
|
||||
// Chargement parallèle tolérant aux pannes réseau / CORS (OpenData SNCF, SIRI-Lite Estimated Timetable, SIRI-Lite Situation Exchange, GTFS-RT)
|
||||
const [jsonRes, rtData, siriData, sxData, openDataPlatforms] = await Promise.all([
|
||||
fetch(stationUrl).catch(err => {
|
||||
// Chargement simultané de toutes les données : Horaires + GTFS-RT + SIRI-LITE
|
||||
const fetchStationJson = async () => {
|
||||
let cached = stationJsonCache.get(String(uic));
|
||||
if (cached) return cached;
|
||||
const jsonRes = await fetch(stationUrl).catch(err => {
|
||||
console.warn(`[sncfApi] Notice: fetch station ${uic} error:`, err);
|
||||
return null;
|
||||
}),
|
||||
fetchGtfsRealtimeData().catch(err => {
|
||||
console.warn('[sncfApi] Notice: GTFS-RT error:', err);
|
||||
return { updates: new Map(), alerts: [] };
|
||||
}),
|
||||
fetchSiriLiteData().catch(err => {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Timetable error:', err);
|
||||
return { platforms: new Map(), updates: new Map() };
|
||||
}),
|
||||
fetchSituationExchangeData().catch(err => {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite Situation Exchange error:', err);
|
||||
return { platforms: new Map(), updates: new Map(), alerts: [] };
|
||||
}),
|
||||
fetchOpenDataSncfPlatforms(uic, mode).catch(err => {
|
||||
console.warn('[sncfApi] Notice: OpenData SNCF platform error:', err);
|
||||
return new Map();
|
||||
})
|
||||
]);
|
||||
});
|
||||
if (jsonRes && jsonRes.ok) {
|
||||
cached = await jsonRes.json();
|
||||
stationJsonCache.set(String(uic), cached);
|
||||
return cached;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if (!jsonRes || !jsonRes.ok) {
|
||||
console.warn(`[sncfApi] Fichier de gare introuvable : ${stationUrl} (statut : ${jsonRes ? jsonRes.status : 'réseau bloqué'})`);
|
||||
const [stationData] = await Promise.all([
|
||||
fetchStationJson(),
|
||||
triggerRealtimeBackgroundRefresh()
|
||||
]);
|
||||
let data = stationData;
|
||||
|
||||
if (!data) {
|
||||
console.warn(`[sncfApi] Fichier de gare introuvable : ${stationUrl}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await jsonRes.json();
|
||||
const rtData = gtfsRtCache || { updates: new Map(), alerts: [] };
|
||||
const siriData = siriLiteCache || { platforms: new Map(), updates: new Map() };
|
||||
const sxData = situationExchangeCache || { platforms: new Map(), updates: new Map(), alerts: [] };
|
||||
const gareRef = findGtfsGare(uic);
|
||||
const targetStationNom = (gareRef && gareRef.nom) || (data && data.nom) || '';
|
||||
const rawList = mode === 'arrivees' ? data.arrivees : data.departs;
|
||||
@@ -840,11 +874,13 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
(uicClean && cNum && sxData.platforms.get(`${uicClean}_${cNum}`)) ||
|
||||
(cNum && cName && sxData.platforms.get(`${cNum}_${cName}`)) ||
|
||||
(rawNum && cName && sxData.platforms.get(`${rawNum}_${cName}`)) ||
|
||||
// 3. OpenData SNCF direct
|
||||
(cNum && uicClean && openDataPlatforms.get(`${cNum}_${uicClean}`)) ||
|
||||
(rawNum && uicClean && openDataPlatforms.get(`${rawNum}_${uicClean}`)) ||
|
||||
(cNum && cName && openDataPlatforms.get(`${cNum}_${cName}`)) ||
|
||||
(cNum && cDest && openDataPlatforms.get(`${cNum}_${cDest}`)) ||
|
||||
// 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) ||
|
||||
''
|
||||
);
|
||||
};
|
||||
@@ -854,8 +890,19 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
const rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr);
|
||||
const timeHM = parseTimeHM(rawTime) || 0;
|
||||
|
||||
const stopUicClean = String(uic || '').replace(/\D/g, '');
|
||||
const stopUic7 = (stopUicClean.length === 8 && stopUicClean.startsWith('87')) ? stopUicClean.slice(2) : '';
|
||||
const cTrainNum = cleanNum(t.num);
|
||||
|
||||
// Recherche de mise à jour GTFS-RT, SIRI-Lite Timetable ou SIRI-Lite Situation Exchange
|
||||
const rt = !isTomorrow ? (rtData.updates.get(t.trip_id) || (t.num ? rtData.updates.get(t.num) : null) || (cleanNum(t.num) ? rtData.updates.get(cleanNum(t.num)) : null)) : null;
|
||||
const rt = !isTomorrow ? (
|
||||
(stopUicClean && cTrainNum && rtData.updates.get(`${cTrainNum}_${stopUicClean}`)) ||
|
||||
(stopUicClean && t.num && rtData.updates.get(`${t.num}_${stopUicClean}`)) ||
|
||||
(stopUic7 && cTrainNum && rtData.updates.get(`${cTrainNum}_${stopUic7}`)) ||
|
||||
rtData.updates.get(t.trip_id) ||
|
||||
(t.num ? rtData.updates.get(t.num) : null) ||
|
||||
(cTrainNum ? rtData.updates.get(cTrainNum) : null)
|
||||
) : null;
|
||||
const siri = !isTomorrow ? (siriData.updates.get(t.num) || (cleanNum(t.num) ? siriData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? siriData.updates.get(t.trip_id) : null)) : null;
|
||||
const sx = !isTomorrow ? (sxData.updates.get(t.num) || (cleanNum(t.num) ? sxData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? sxData.updates.get(t.trip_id) : null)) : null;
|
||||
|
||||
@@ -886,7 +933,13 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
statusLabel = 'supprimé';
|
||||
etat = 'supprime';
|
||||
} else if (delayMinutes > 0) {
|
||||
statusLabel = `retardé +${delayMinutes}min.`;
|
||||
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.`;
|
||||
}
|
||||
etat = 'retarde';
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* Base de données exhaustive des plages de numérotation GTFS SNCF
|
||||
* Source : GTFS SNCF Open Data (feed valide 02/10/2026 au 31/03/2027)
|
||||
*
|
||||
* 1. Cars TER Réguliers (route_type = 3)
|
||||
* 2. Cars de Substitution Travaux (route_type = 2)
|
||||
* 3. Trains TER (route_type = 2)
|
||||
*/
|
||||
|
||||
export const REGIONS_META = {
|
||||
AURA: { code: 'AURA', name: 'Auvergne-Rhône-Alpes', brand: 'TER AURA', logo: 'logo-auvergerhonealpes.svg', color: '#0077c8', cssClass: 'brand-teraura' },
|
||||
BFC: { code: 'BFC', name: 'Bourgogne-Franche-Comté', brand: 'MOBIGO', logo: 'logo-mobigo.svg', color: '#cfd82c', cssClass: 'brand-mobigo' },
|
||||
BRE: { code: 'BRE', name: 'Bretagne', brand: 'BREIZHGO', logo: 'logo-breizhgo.svg', color: '#00c6d3', cssClass: 'brand-breizhgo' },
|
||||
CVL: { code: 'CVL', name: 'Centre-Val de Loire', brand: 'RÉMI', logo: 'logo-remi.svg', color: '#00874e', cssClass: 'brand-remi' },
|
||||
GE: { code: 'GE', name: 'Grand Est', brand: 'FLUO', logo: 'logo-fluo.svg', color: '#f78bb8', cssClass: 'brand-fluo' },
|
||||
HDF: { code: 'HDF', name: 'Hauts-de-France', brand: 'TER HDF', logo: 'logo-hautsdefrance.svg', color: '#c9e000', cssClass: 'brand-terhdf' },
|
||||
NOR: { code: 'NOR', name: 'Normandie', brand: 'NOMAD', logo: 'logo-nomad.svg', color: '#6bb07a', cssClass: 'brand-nomad' },
|
||||
NA: { code: 'NA', name: 'Nouvelle-Aquitaine', brand: 'TER NA', logo: 'logo-teraquitaine.svg', color: '#ea5b0c', cssClass: 'brand-terna' },
|
||||
OCC: { code: 'OCC', name: 'Occitanie', brand: 'liO', logo: 'logo-lio.svg', color: '#e3001b', cssClass: 'brand-lio' },
|
||||
PDL: { code: 'PDL', name: 'Pays de la Loire', brand: 'ALÉOP', logo: 'logo-aleop.svg', color: '#00b0ca', cssClass: 'brand-aleop' },
|
||||
PACA: { code: 'PACA', name: "Provence-Alpes-Côte d'Azur", brand: 'ZOU !', logo: 'logo-zou.svg', color: '#0055a4', cssClass: 'brand-zou' },
|
||||
IDF: { code: 'IDF', name: 'Île-de-France', brand: 'TRANSILIEN', logo: 'logo-sncf.svg', color: '#003b80', cssClass: 'brand-idf' }
|
||||
};
|
||||
|
||||
// 1. CARS TER RÉGULIERS (route_type = 3)
|
||||
export const CARS_REGULIERS_RANGES = [
|
||||
{ min: 30131, max: 31478, region: 'AURA' },
|
||||
{ min: 33155, max: 35072, region: 'AURA' },
|
||||
{ min: 405036, max: 405562, region: 'AURA' },
|
||||
{ min: 35000, max: 35065, region: 'BFC' },
|
||||
{ min: 36518, max: 37529, region: 'BFC' },
|
||||
{ min: 48022, max: 48027, region: 'BRE' },
|
||||
{ min: 424300, max: 424336, region: 'CVL' },
|
||||
{ min: 426210, max: 426223, region: 'CVL' },
|
||||
{ min: 43001, max: 45141, region: 'GE' },
|
||||
{ min: 68130, max: 68283, region: 'GE' },
|
||||
{ min: 428429, max: 429499, region: 'GE' },
|
||||
{ min: 45530, max: 45569, region: 'HDF' },
|
||||
{ min: 69400, max: 69830, region: 'HDF' },
|
||||
{ min: 48000, max: 48033, region: 'NOR' },
|
||||
{ min: 87002, max: 88061, region: 'NOR' },
|
||||
{ min: 49957, max: 49974, region: 'NA' },
|
||||
{ min: 70926, max: 70944, region: 'NA' },
|
||||
{ min: 94000, max: 94032, region: 'OCC' },
|
||||
{ min: 95201, max: 95477, region: 'OCC' },
|
||||
{ min: 457156, max: 457268, region: 'OCC' },
|
||||
{ min: 461900, max: 462402, region: 'OCC' },
|
||||
{ min: 49981, max: 49986, region: 'PDL' },
|
||||
{ min: 75000, max: 75039, region: 'PDL' }
|
||||
];
|
||||
|
||||
// 2. CARS DE SUBSTITUTION TRAVAUX (route_type = 2)
|
||||
export const CARS_SUBSTITUTION_RANGES = [
|
||||
{ min: 30000, max: 31497, region: 'AURA' },
|
||||
{ min: 33005, max: 34813, region: 'AURA' },
|
||||
{ min: 404001, max: 412498, region: 'AURA' },
|
||||
{ min: 35039, max: 35044, region: 'BFC' },
|
||||
{ min: 36501, max: 37304, region: 'BFC' },
|
||||
{ min: 415035, max: 418367, region: 'BFC' },
|
||||
{ min: 419501, max: 419627, region: 'BFC' },
|
||||
{ min: 424010, max: 424403, region: 'BFC' },
|
||||
{ min: 67864, max: 67872, region: 'BRE' },
|
||||
{ min: 421001, max: 422112, region: 'BRE' },
|
||||
{ min: 442003, max: 442008, region: 'BRE' },
|
||||
{ min: 40850, max: 40904, region: 'CVL' },
|
||||
{ min: 424020, max: 426502, region: 'CVL' },
|
||||
{ min: 43103, max: 45499, region: 'GE' },
|
||||
{ min: 427250, max: 435837, region: 'GE' },
|
||||
{ min: 45500, max: 46576, region: 'HDF' },
|
||||
{ min: 85564, max: 85661, region: 'HDF' },
|
||||
{ min: 436010, max: 436191, region: 'HDF' },
|
||||
{ min: 437480, max: 437845, region: 'HDF' },
|
||||
{ min: 439005, max: 439537, region: 'HDF' },
|
||||
{ min: 48001, max: 48271, region: 'NOR' },
|
||||
{ min: 442108, max: 442129, region: 'NOR' },
|
||||
{ min: 446802, max: 447248, region: 'NOR' },
|
||||
{ min: 70452, max: 70769, region: 'NA' },
|
||||
{ min: 448014, max: 448812, region: 'NA' },
|
||||
{ min: 450001, max: 451482, region: 'NA' },
|
||||
{ min: 453009, max: 453872, region: 'NA' },
|
||||
{ min: 455006, max: 455684, region: 'NA' },
|
||||
{ min: 60401, max: 60444, region: 'OCC' },
|
||||
{ min: 73100, max: 73303, region: 'OCC' },
|
||||
{ min: 95470, max: 95478, region: 'OCC' },
|
||||
{ min: 412062, max: 412494, region: 'OCC' },
|
||||
{ min: 455005, max: 455238, region: 'OCC' },
|
||||
{ min: 457001, max: 457452, region: 'OCC' },
|
||||
{ min: 459663, max: 459691, region: 'OCC' },
|
||||
{ min: 461001, max: 462221, region: 'OCC' },
|
||||
{ min: 464040, max: 464140, region: 'PDL' },
|
||||
{ min: 64001, max: 64006, region: 'PACA' },
|
||||
{ min: 79000, max: 79002, region: 'PACA' },
|
||||
{ min: 466752, max: 468300, region: 'PACA' },
|
||||
{ min: 445602, max: 445619, region: 'IDF' }
|
||||
];
|
||||
|
||||
// 3. TRAINS TER (route_type = 2)
|
||||
export const TRAINS_TER_RANGES = [
|
||||
// 6 CHIFFRES (Segmentation très précise)
|
||||
{ min: 830102, max: 836407, region: 'GE' },
|
||||
{ min: 837501, max: 840877, region: 'GE' },
|
||||
{ min: 841200, max: 844948, region: 'HDF' },
|
||||
{ min: 846400, max: 849966, region: 'HDF' },
|
||||
{ min: 848950, max: 848977, region: 'NOR' },
|
||||
{ min: 850016, max: 853728, region: 'NOR' },
|
||||
{ min: 854007, max: 858397, region: 'BRE' },
|
||||
{ min: 857000, max: 860993, region: 'PDL' },
|
||||
{ min: 860001, max: 863714, region: 'CVL' },
|
||||
{ min: 862551, max: 862589, region: 'PDL' },
|
||||
{ min: 863031, max: 869129, region: 'NA' },
|
||||
{ min: 870000, max: 872891, region: 'OCC' },
|
||||
{ min: 873901, max: 873994, region: 'OCC' },
|
||||
{ min: 876100, max: 878991, region: 'OCC' },
|
||||
{ min: 873003, max: 874231, region: 'AURA' },
|
||||
{ min: 879102, max: 881699, region: 'PACA' },
|
||||
{ min: 882103, max: 890392, region: 'AURA' },
|
||||
{ min: 891000, max: 895975, region: 'BFC' },
|
||||
|
||||
// 4 ET 5 CHIFFRES
|
||||
{ min: 3100, max: 3483, region: 'NOR' },
|
||||
{ min: 3903, max: 3924, region: 'CVL' },
|
||||
{ min: 5900, max: 5921, region: 'BFC' },
|
||||
{ min: 13100, max: 13191, region: 'NOR' },
|
||||
{ min: 16350, max: 16480, region: 'HDF' },
|
||||
{ min: 16521, max: 16587, region: 'NOR' },
|
||||
{ min: 16843, max: 18999, region: 'AURA' },
|
||||
{ min: 17353, max: 17705, region: 'PACA' },
|
||||
{ min: 17750, max: 18146, region: 'BFC' },
|
||||
{ min: 19701, max: 19979, region: 'HDF' },
|
||||
{ min: 22244, max: 23026, region: 'OCC' },
|
||||
{ min: 23101, max: 23794, region: 'AURA' },
|
||||
{ min: 23722, max: 23831, region: 'GE' },
|
||||
{ min: 33501, max: 33647, region: 'PACA' },
|
||||
{ min: 81390, max: 81496, region: 'GE' },
|
||||
{ min: 86002, max: 86104, region: 'PACA' },
|
||||
{ min: 86380, max: 87470, region: 'GE' },
|
||||
{ min: 86960, max: 86998, region: 'OCC' },
|
||||
{ min: 88488, max: 88877, region: 'GE' },
|
||||
{ min: 96100, max: 96299, region: 'GE' },
|
||||
{ min: 96500, max: 96769, region: 'AURA' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Détecte la région probable à partir du nom ou de l'UIC d'une gare
|
||||
*/
|
||||
export function guessRegionFromStation(stationNameOrUic) {
|
||||
if (!stationNameOrUic) return null;
|
||||
const s = String(stationNameOrUic).toLowerCase();
|
||||
|
||||
if (s.includes('dijon') || s.includes('besançon') || s.includes('belfort') || s.includes('chalon') || s.includes('nevers') || s.includes('auxerre') || s.includes('mâcon') || s.includes('sens') || s.includes('bourgogne') || s.includes('franche-comte')) return 'BFC';
|
||||
if (s.includes('lyon') || s.includes('grenoble') || s.includes('saint-etienne') || s.includes('clermont') || s.includes('chambery') || s.includes('annecy') || s.includes('valence') || s.includes('aura') || s.includes('alpes')) return 'AURA';
|
||||
if (s.includes('marseille') || s.includes('nice') || s.includes('toulon') || s.includes('avignon') || s.includes('cannes') || s.includes('arles') || s.includes('antibes') || s.includes('paca') || s.includes('azur')) return 'PACA';
|
||||
if (s.includes('toulouse') || s.includes('montpellier') || s.includes('nimes') || s.includes('perpignan') || s.includes('beziers') || s.includes('narbonne') || s.includes('tarbes') || s.includes('occitanie')) return 'OCC';
|
||||
if (s.includes('bordeaux') || s.includes('limoges') || s.includes('poitiers') || s.includes('la rochelle') || s.includes('pau') || s.includes('angouleme') || s.includes('bayonne') || s.includes('agen') || s.includes('aquitaine')) return 'NA';
|
||||
if (s.includes('strasbourg') || s.includes('metz') || s.includes('nancy') || s.includes('reims') || s.includes('mulhouse') || s.includes('colmar') || s.includes('troyes') || s.includes('charleville') || s.includes('alsace') || s.includes('lorraine')) return 'GE';
|
||||
if (s.includes('lille') || s.includes('amiens') || s.includes('calais') || s.includes('dunkerque') || s.includes('arras') || s.includes('valenciennes') || s.includes('douai') || s.includes('hauts-de-france') || s.includes('nord')) return 'HDF';
|
||||
if (s.includes('rouen') || s.includes('caen') || s.includes('le havre') || s.includes('cherbourg') || s.includes('evreux') || s.includes('dieppe') || s.includes('alencon') || s.includes('normandie')) return 'NOR';
|
||||
if (s.includes('rennes') || s.includes('brest') || s.includes('quimper') || s.includes('vannes') || s.includes('saint-brieuc') || s.includes('lorient') || s.includes('saint-malo') || s.includes('bretagne')) return 'BRE';
|
||||
if (s.includes('nantes') || s.includes('angers') || s.includes('le mans') || s.includes('saint-nazaire') || s.includes('la roche-sur-yon') || s.includes('cholet') || s.includes('laval') || s.includes('loire')) return 'PDL';
|
||||
if (s.includes('orleans') || s.includes('tours') || s.includes('bourges') || s.includes('blois') || s.includes('chartres') || s.includes('chateauroux') || s.includes('vierzon')) return 'CVL';
|
||||
if (s.includes('paris') || s.includes('saint-denis') || s.includes('versailles') || s.includes('melun')) return 'IDF';
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moteur de résolution d'identité de circulation GTFS
|
||||
* @param {string|number} trainNumStr
|
||||
* @param {Object} options { stationContext, explicitMode }
|
||||
*/
|
||||
export function resolveCirculationIdentity(trainNumStr, options = {}) {
|
||||
const { stationContext = null, explicitMode = '' } = options;
|
||||
const num = parseInt(String(trainNumStr || '').replace(/\D/g, ''), 10);
|
||||
const preferredRegion = guessRegionFromStation(stationContext);
|
||||
const modeLower = String(explicitMode || '').toLowerCase();
|
||||
|
||||
const isExplicitCar = modeLower.includes('car') || modeLower.includes('autocar') || modeLower.includes('bus');
|
||||
|
||||
if (!isNaN(num)) {
|
||||
// 1. Priorité A : Cars de substitution travaux (route_type = 2)
|
||||
const subMatches = CARS_SUBSTITUTION_RANGES.filter(r => num >= r.min && num <= r.max);
|
||||
if (subMatches.length > 0) {
|
||||
const best = (preferredRegion && subMatches.find(m => m.region === preferredRegion)) || subMatches[0];
|
||||
const meta = REGIONS_META[best.region] || REGIONS_META.AURA;
|
||||
return {
|
||||
isCar: true,
|
||||
isSubstitution: true,
|
||||
regionCode: best.region,
|
||||
brand: meta.brand,
|
||||
typeLabel: 'CAR TER',
|
||||
logoFile: 'sncf-voyageurs-logo.png',
|
||||
cssClass: meta.cssClass,
|
||||
color: meta.color
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Priorité B : Cars TER Réguliers (route_type = 3)
|
||||
const regCarMatches = CARS_REGULIERS_RANGES.filter(r => num >= r.min && num <= r.max);
|
||||
if (regCarMatches.length > 0) {
|
||||
const best = (preferredRegion && regCarMatches.find(m => m.region === preferredRegion)) || regCarMatches[0];
|
||||
const meta = REGIONS_META[best.region] || REGIONS_META.AURA;
|
||||
return {
|
||||
isCar: true,
|
||||
isSubstitution: false,
|
||||
regionCode: best.region,
|
||||
brand: meta.brand,
|
||||
typeLabel: 'CAR',
|
||||
logoFile: 'track-car.svg',
|
||||
cssClass: meta.cssClass,
|
||||
color: meta.color
|
||||
};
|
||||
}
|
||||
|
||||
// Si le numéro est typique des séries autocar (4xxxxx), forcer Car même hors intervalle strict
|
||||
if (num >= 400000 && num <= 469999) {
|
||||
return {
|
||||
isCar: true,
|
||||
isSubstitution: true,
|
||||
regionCode: preferredRegion || 'AURA',
|
||||
brand: preferredRegion && REGIONS_META[preferredRegion] ? REGIONS_META[preferredRegion].brand : 'CAR TER',
|
||||
typeLabel: 'CAR',
|
||||
logoFile: 'track-car.svg',
|
||||
cssClass: preferredRegion && REGIONS_META[preferredRegion] ? REGIONS_META[preferredRegion].cssClass : 'brand-teraura',
|
||||
color: '#0077c8'
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Priorité C : Trains TER (route_type = 2)
|
||||
const trainMatches = TRAINS_TER_RANGES.filter(r => num >= r.min && num <= r.max);
|
||||
if (trainMatches.length > 0) {
|
||||
// Si conflit de chevauchement, prioriser la région de la gare si elle correspond
|
||||
const best = (preferredRegion && trainMatches.find(m => m.region === preferredRegion)) ||
|
||||
// Sinon favoriser la plage la plus spécifique (écart min/max le plus petit)
|
||||
trainMatches.sort((a, b) => (a.max - a.min) - (b.max - b.min))[0];
|
||||
|
||||
const meta = REGIONS_META[best.region] || REGIONS_META.AURA;
|
||||
return {
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
regionCode: best.region,
|
||||
brand: meta.brand,
|
||||
typeLabel: meta.brand,
|
||||
logoFile: meta.logo,
|
||||
cssClass: meta.cssClass,
|
||||
color: meta.color
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback si rien n'a matché
|
||||
if (isExplicitCar) {
|
||||
return {
|
||||
isCar: true,
|
||||
isSubstitution: false,
|
||||
regionCode: preferredRegion || 'SNCF',
|
||||
brand: 'Car TER',
|
||||
typeLabel: 'CAR',
|
||||
logoFile: 'track-car.svg',
|
||||
cssClass: 'brand-car',
|
||||
color: '#666666'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
regionCode: preferredRegion || 'SNCF',
|
||||
brand: 'TER',
|
||||
typeLabel: 'TER',
|
||||
logoFile: 'logo-ter.svg',
|
||||
cssClass: 'brand-ter',
|
||||
color: '#0077c8'
|
||||
};
|
||||
}
|
||||
+183
-148
@@ -1,3 +1,5 @@
|
||||
import { resolveCirculationIdentity } from './gtfsRegionalRanges';
|
||||
|
||||
// Indexation optimisée des logos pour recherche rapide et insensible à la casse / accents
|
||||
const normalizeStr = (s) => {
|
||||
if (!s) return '';
|
||||
@@ -11,7 +13,7 @@ const normalizeStr = (s) => {
|
||||
// Table des logos issue de public/type/data.json
|
||||
const LOGOS_DATA = [
|
||||
{ file: "logo-aleop.svg", slug: "aleop", name: "Aleop" },
|
||||
{ file: "sncf-voyageurs-logo.png", slug: "auvergerhonealpes", name: "TER AURA" },
|
||||
{ file: "logo-auvergerhonealpes.svg", slug: "auvergerhonealpes", name: "TER AURA" },
|
||||
{ file: "logo-breizhgo.svg", slug: "breizhgo", name: "Breizhgo" },
|
||||
{ file: "logo-cfl.svg", slug: "cfl", name: "CFL" },
|
||||
{ file: "logo-db.svg", slug: "db", name: "DB" },
|
||||
@@ -43,28 +45,16 @@ const LOGOS_DATA = [
|
||||
{ file: "logo-sncf1985.svg", slug: "sncf1985", name: "Sncf1985" },
|
||||
{ file: "logo-sncf1992.svg", slug: "sncf1992", name: "Sncf1992" },
|
||||
{ file: "logo-teoz.svg", slug: "teoz", name: "Teoz" },
|
||||
|
||||
{ file: "logo-ter-rhone-alpes.svg", slug: "ter-rhone-alpes", name: "TER Rhone-Alpes" },
|
||||
{ file: "logo-teralsace.svg", slug: "teralsace", name: "Ter Alsace" },
|
||||
{ file: "logo-teraquitaine.svg", slug: "teraquitaine", name: "Ter Aquitaine" },
|
||||
{ file: "logo-terbassenormandie.svg", slug: "terbassenormandie", name: "Ter Basse Normandie" },
|
||||
{ file: "logo-terbourgogne.svg", slug: "terbourgogne", name: "Ter Bourgogne" },
|
||||
{ file: "logo-terbretagne.svg", slug: "terbretagne", name: "Ter Bretagne" },
|
||||
{ file: "logo-tercentre.svg", slug: "tercentre", name: "Ter Centre" },
|
||||
{ file: "logo-terlanguedocroussillon.svg", slug: "terlanguedocroussillon", name: "Ter Languedoc-Roussillon" },
|
||||
{ file: "logo-termidipyrenees.svg", slug: "termidipyrenees", name: "Ter Midi-Pyrenees" },
|
||||
{ file: "logo-ternordpasdecalais.svg", slug: "ternordpasdecalais", name: "Ter Nord-pas-de-Calais" },
|
||||
{ file: "logo-terpoitoucharentes.svg", slug: "terpoitoucharentes", name: "Ter Poitou Charentes" },
|
||||
{ file: "logo-tgv.svg", slug: "tgv", name: "TGV" },
|
||||
{ file: "logo-inoui.svg", slug: "inoui", name: "TGV InOui" },
|
||||
{ file: "logo-lyria.svg", slug: "lyria", name: "TGV Lyria" },
|
||||
{ file: "logo-thalys.svg", slug: "thalys", name: "Thalys" },
|
||||
{ file: "logo-thello.svg", slug: "thello", name: "Thello" },
|
||||
{ file: "track-car.svg", slug: "track-car", name: "Car TER" },
|
||||
{ file: "logo-tramtrain.svg", slug: "tramtrain", name: "Tramtrain" },
|
||||
{ file: "logo-trenitalia.svg", slug: "trenitalia", name: "Trenitalia" },
|
||||
{ file: "logo-zou.svg", slug: "zou", name: "Zou" }
|
||||
|
||||
];
|
||||
|
||||
const logoLookup = new Map();
|
||||
@@ -82,11 +72,24 @@ LOGOS_DATA.forEach((item) => {
|
||||
|
||||
/**
|
||||
* Détermine le logo SVG / PNG et le libellé de train à afficher à partir d'un objet horaire
|
||||
* Supporte les données SNCF Open Data (GTFS / SIRI) et Supabase
|
||||
* Intègre la détection déterministe GTFS (Cars réguliers, Cars substitution, Trains régionaux)
|
||||
* @param {Object} horaire
|
||||
* @param {string} [stationContext] - Nom de la gare courante pour arbitrage géographique
|
||||
*/
|
||||
export function getTrainLogoAndMeta(horaire) {
|
||||
export function getTrainLogoAndMeta(horaire, stationContext = null) {
|
||||
if (!horaire) {
|
||||
return { logoSrc: '/type/sncf-voyageurs-logo.png', typeLabel: 'TER', numTrain: '' };
|
||||
return {
|
||||
logoSrc: '/type/logo-ter.svg',
|
||||
logoFile: 'logo-ter.svg',
|
||||
typeLabel: 'TER',
|
||||
numTrain: '',
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'TER',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-ter',
|
||||
brandColor: '#0077c8'
|
||||
};
|
||||
}
|
||||
|
||||
const opRaw = String(
|
||||
@@ -109,141 +112,173 @@ export function getTrainLogoAndMeta(horaire) {
|
||||
|
||||
const opNorm = normalizeStr(opRaw);
|
||||
|
||||
// 1. Recherche directe dans la base des logos
|
||||
let matchedLogo = logoLookup.get(opNorm);
|
||||
|
||||
// 2. Recherche par mots-clés si pas de correspondance exacte
|
||||
if (!matchedLogo) {
|
||||
if (opNorm.includes('inoui') || opNorm.includes('tgv')) {
|
||||
matchedLogo = logoLookup.get('inoui') || logoLookup.get('tgv');
|
||||
} else if (opNorm.includes('ouigoclassique')) {
|
||||
matchedLogo = logoLookup.get('ouigoclassique');
|
||||
} else if (opNorm.includes('ouigo')) {
|
||||
matchedLogo = logoLookup.get('ouigo');
|
||||
} else if (opNorm.includes('mobigo')) {
|
||||
matchedLogo = logoLookup.get('mobigo');
|
||||
} else if (opNorm.includes('fluo')) {
|
||||
matchedLogo = logoLookup.get('fluo');
|
||||
} else if (opNorm.includes('remi')) {
|
||||
matchedLogo = logoLookup.get('remi');
|
||||
} else if (opNorm.includes('nomad')) {
|
||||
matchedLogo = logoLookup.get('nomad');
|
||||
} else if (opNorm.includes('aleop')) {
|
||||
matchedLogo = logoLookup.get('aleop');
|
||||
} else if (opNorm.includes('breizhgo')) {
|
||||
matchedLogo = logoLookup.get('breizhgo');
|
||||
} else if (opNorm.includes('hautsdefrance')) {
|
||||
matchedLogo = logoLookup.get('hautsdefrance');
|
||||
} else if (opNorm.includes('lio')) {
|
||||
matchedLogo = logoLookup.get('lio');
|
||||
} else if (opNorm.includes('zou')) {
|
||||
matchedLogo = logoLookup.get('zou');
|
||||
} else if (opNorm.includes('intercites') || opNorm.includes('ic')) {
|
||||
matchedLogo = logoLookup.get('intercites');
|
||||
} else if (opNorm.includes('ice')) {
|
||||
matchedLogo = logoLookup.get('ice');
|
||||
} else if (opNorm.includes('eurostar')) {
|
||||
matchedLogo = logoLookup.get('eurostar');
|
||||
} else if (opNorm.includes('thalys')) {
|
||||
matchedLogo = logoLookup.get('thalys');
|
||||
} else if (opNorm.includes('lyria')) {
|
||||
matchedLogo = logoLookup.get('lyria');
|
||||
} else if (opNorm.includes('frecciarossa') || opNorm.includes('trenitalia')) {
|
||||
matchedLogo = logoLookup.get('frecciarossa') || logoLookup.get('trenitalia');
|
||||
} else if (opNorm.includes('renfe') || opNorm.includes('ave')) {
|
||||
matchedLogo = logoLookup.get('renfeave');
|
||||
} else if (opNorm.includes('car') || opNorm.includes('autocar') || opNorm.includes('bus')) {
|
||||
matchedLogo = logoLookup.get('trackcar') || { file: 'sncf-voyageurs-logo.png', name: 'Car TER' };
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Détermination du libellé affiché (typeLabel)
|
||||
let typeLabel = opRaw || 'TER';
|
||||
|
||||
// 1. Grands transporteurs nationaux et internationaux prioritaires (Explicites)
|
||||
if (opNorm.includes('inoui') || opNorm.includes('tgvinoui')) {
|
||||
typeLabel = 'TGV inOUI';
|
||||
} else if (opNorm === 'tgv') {
|
||||
typeLabel = 'TGV';
|
||||
} else if (opNorm.includes('ouigoclassique')) {
|
||||
typeLabel = 'OUIGO Train Classique';
|
||||
} else if (opNorm.includes('ouigo')) {
|
||||
typeLabel = 'OUIGO';
|
||||
} else if (opNorm.includes('mobigo')) {
|
||||
typeLabel = 'MOBIGO';
|
||||
} else if (opNorm.includes('fluo')) {
|
||||
typeLabel = 'FLUO';
|
||||
} else if (opNorm.includes('nomad')) {
|
||||
typeLabel = 'NOMAD';
|
||||
} else if (opNorm.includes('remi')) {
|
||||
typeLabel = 'RÉMI';
|
||||
} else if (opNorm.includes('aleop')) {
|
||||
typeLabel = 'ALÉOP';
|
||||
} else if (opNorm.includes('breizhgo')) {
|
||||
typeLabel = 'BREIZHGO';
|
||||
} else if (opNorm.includes('hautsdefrance')) {
|
||||
typeLabel = 'TER HDF';
|
||||
} else if (opNorm.includes('lio')) {
|
||||
typeLabel = 'LIÒ';
|
||||
} else if (opNorm.includes('zou')) {
|
||||
typeLabel = 'ZOU !';
|
||||
} else if (opNorm.includes('intercites') || opNorm === 'ic') {
|
||||
typeLabel = 'INTERCITÉS';
|
||||
} else if (opNorm.includes('ice')) {
|
||||
typeLabel = 'ICE';
|
||||
} else if (opNorm.includes('eurostar')) {
|
||||
typeLabel = 'EUROSTAR';
|
||||
} else if (opNorm.includes('lyria')) {
|
||||
typeLabel = 'TGV Lyria';
|
||||
} else if (opNorm.includes('car') || opNorm.includes('autocar') || opNorm.includes('bus')) {
|
||||
typeLabel = 'Car TER';
|
||||
} else if (opNorm.includes('ter')) {
|
||||
typeLabel = 'TER';
|
||||
return {
|
||||
logoSrc: '/type/logo-inoui.svg',
|
||||
logoFile: 'logo-inoui.svg',
|
||||
typeLabel: 'TGV inOUI',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'TGV inOUI',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-inoui',
|
||||
brandColor: '#7a1f3d'
|
||||
};
|
||||
}
|
||||
|
||||
// Application du plan de numérotation déterministe TER
|
||||
if ((typeLabel === 'TER' || typeLabel === 'AUTOCAR' || typeLabel === 'Car TER') && numTrain) {
|
||||
const cleanNum = numTrain.replace(/\D/g, '');
|
||||
let matchedBrandSlug = null;
|
||||
let matchedBrandLabel = null;
|
||||
|
||||
if (cleanNum.length >= 4) {
|
||||
const p3 = parseInt(cleanNum.substring(0, 3), 10);
|
||||
const p4 = parseInt(cleanNum.substring(0, 4), 10);
|
||||
|
||||
if ((p3 >= 830 && p3 <= 840) || (p3 >= 160 && p3 <= 162)) {
|
||||
matchedBrandSlug = 'fluo'; matchedBrandLabel = 'FLUO';
|
||||
} else if ((p3 >= 841 && p3 <= 849) || p3 === 163 || p3 === 164) {
|
||||
matchedBrandSlug = 'hautsdefrance'; matchedBrandLabel = 'TER HDF';
|
||||
} else if ((p3 >= 850 && p3 <= 853) || p3 === 165) {
|
||||
matchedBrandSlug = 'nomad'; matchedBrandLabel = 'NOMAD';
|
||||
} else if ((p3 >= 854 && p3 <= 856) || (p4 >= 1660 && p4 <= 1664)) {
|
||||
matchedBrandSlug = 'breizhgo'; matchedBrandLabel = 'BREIZHGO';
|
||||
} else if ((p3 >= 857 && p3 <= 859) || (p4 >= 1665 && p4 <= 1674)) {
|
||||
matchedBrandSlug = 'aleop'; matchedBrandLabel = 'ALÉOP';
|
||||
} else if ((p3 >= 860 && p3 <= 862) || (p4 >= 1675 && p4 <= 1684)) {
|
||||
matchedBrandSlug = 'remi'; matchedBrandLabel = 'RÉMI';
|
||||
} else if ((p3 >= 863 && p3 <= 869) || (p4 >= 1685 && p4 <= 1709)) {
|
||||
matchedBrandSlug = 'teraquitaine'; matchedBrandLabel = 'TER NA';
|
||||
} else if ((p3 >= 870 && p3 <= 872) || (p3 >= 876 && p3 <= 878) || (p4 >= 1710 && p4 <= 1719) || (p4 >= 1730 && p4 <= 1734)) {
|
||||
matchedBrandSlug = 'lio'; matchedBrandLabel = 'liO';
|
||||
} else if ((p3 >= 873 && p3 <= 875) || (p3 >= 882 && p3 <= 890) || (p4 >= 1720 && p4 <= 1729) || (p4 >= 1750 && p4 <= 1774) || (p4 >= 1790 && p4 <= 1799)) {
|
||||
matchedBrandSlug = 'ter-rhone-alpes'; matchedBrandLabel = 'TER AURA';
|
||||
} else if ((p3 >= 879 && p3 <= 881) || (p4 >= 1735 && p4 <= 1749)) {
|
||||
matchedBrandSlug = 'zou'; matchedBrandLabel = 'ZOU !';
|
||||
} else if ((p3 >= 891 && p3 <= 895) || (p4 >= 1775 && p4 <= 1789)) {
|
||||
matchedBrandSlug = 'mobigo'; matchedBrandLabel = 'MOBIGO';
|
||||
}
|
||||
|
||||
if (matchedBrandSlug && matchedBrandLabel) {
|
||||
typeLabel = matchedBrandLabel;
|
||||
if (!matchedLogo || matchedLogo.file === 'logo-sncf.svg' || matchedLogo.file === 'logo-sncf.svg') {
|
||||
matchedLogo = logoLookup.get(matchedBrandSlug) || matchedLogo;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (opNorm === 'tgv' || (opNorm.includes('tgv') && !opNorm.includes('ouigo') && !opNorm.includes('lyria'))) {
|
||||
return {
|
||||
logoSrc: '/type/logo-tgv.svg',
|
||||
logoFile: 'logo-tgv.svg',
|
||||
typeLabel: 'TGV',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'TGV',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-tgv',
|
||||
brandColor: '#6e1e24'
|
||||
};
|
||||
}
|
||||
const logoFile = (matchedLogo && matchedLogo.file) || 'sncf-voyageurs-logo.png';
|
||||
const logoSrc = `/type/${logoFile}`;
|
||||
|
||||
return { logoSrc, typeLabel, numTrain };
|
||||
if (opNorm.includes('ouigoclassique') || opNorm.includes('ouigotrainclassique')) {
|
||||
return {
|
||||
logoSrc: '/type/logo-ouigo-classique.svg',
|
||||
logoFile: 'logo-ouigo-classique.svg',
|
||||
typeLabel: 'OUIGO Classique',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'OUIGO Classique',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-ouigo-classique',
|
||||
brandColor: '#ff007f'
|
||||
};
|
||||
}
|
||||
|
||||
if (opNorm.includes('ouigo')) {
|
||||
return {
|
||||
logoSrc: '/type/logo-ouigo.svg',
|
||||
logoFile: 'logo-ouigo.svg',
|
||||
typeLabel: 'OUIGO',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'OUIGO',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-ouigo',
|
||||
brandColor: '#00a3e0'
|
||||
};
|
||||
}
|
||||
|
||||
if (opNorm.includes('intercites') || opNorm === 'ic') {
|
||||
return {
|
||||
logoSrc: '/type/logo-intercites.svg',
|
||||
logoFile: 'logo-intercites.svg',
|
||||
typeLabel: 'INTERCITÉS',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'INTERCITÉS',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-intercites',
|
||||
brandColor: '#a61c28'
|
||||
};
|
||||
}
|
||||
|
||||
if (opNorm.includes('eurostar')) {
|
||||
return {
|
||||
logoSrc: '/type/logo-eurostar.svg',
|
||||
logoFile: 'logo-eurostar.svg',
|
||||
typeLabel: 'EUROSTAR',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'EUROSTAR',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-eurostar',
|
||||
brandColor: '#003865'
|
||||
};
|
||||
}
|
||||
|
||||
if (opNorm.includes('ice') || opNorm.includes('db')) {
|
||||
return {
|
||||
logoSrc: '/type/logo-ice.svg',
|
||||
logoFile: 'logo-ice.svg',
|
||||
typeLabel: 'ICE',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'ICE',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-ice',
|
||||
brandColor: '#eb1800'
|
||||
};
|
||||
}
|
||||
|
||||
if (opNorm.includes('lyria')) {
|
||||
return {
|
||||
logoSrc: '/type/logo-lyria.svg',
|
||||
logoFile: 'logo-lyria.svg',
|
||||
typeLabel: 'TGV Lyria',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'TGV Lyria',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-lyria',
|
||||
brandColor: '#b4192b'
|
||||
};
|
||||
}
|
||||
|
||||
if (opNorm.includes('frecciarossa') || opNorm.includes('trenitalia')) {
|
||||
return {
|
||||
logoSrc: '/type/logo-frecciarossa.svg',
|
||||
logoFile: 'logo-frecciarossa.svg',
|
||||
typeLabel: 'Frecciarossa',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'Trenitalia',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-trenitalia',
|
||||
brandColor: '#c8102e'
|
||||
};
|
||||
}
|
||||
|
||||
if (opNorm.includes('renfe') || opNorm.includes('ave')) {
|
||||
return {
|
||||
logoSrc: '/type/logo-renfeave.svg',
|
||||
logoFile: 'logo-renfeave.svg',
|
||||
typeLabel: 'AVE',
|
||||
numTrain,
|
||||
isCar: false,
|
||||
isSubstitution: false,
|
||||
brand: 'Renfe',
|
||||
regionCode: null,
|
||||
brandClass: 'brand-renfe',
|
||||
brandColor: '#6f2c91'
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Moteur déterministe GTFS : Déduction par numéro de train et contexte de gare
|
||||
const identity = resolveCirculationIdentity(numTrain, {
|
||||
stationContext: stationContext || horaire.gare_depart_nom || horaire.gare_arrivee_nom || '',
|
||||
explicitMode: opRaw
|
||||
});
|
||||
|
||||
return {
|
||||
logoSrc: `/type/${identity.logoFile}`,
|
||||
logoFile: identity.logoFile,
|
||||
typeLabel: identity.typeLabel,
|
||||
numTrain,
|
||||
isCar: identity.isCar,
|
||||
isSubstitution: identity.isSubstitution,
|
||||
brand: identity.brand,
|
||||
regionCode: identity.regionCode,
|
||||
brandClass: identity.cssClass,
|
||||
brandColor: identity.color
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user