Fix: add missing services files
This commit is contained in:
1 parent
33e97f2f3f
commit
bf6d8722e0
5 files changed
+25534
No files matched your search
@@ -0,0 +1,78 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { getSourceFromPath, getTimetable, getStationMeta } from '../services/railwayService';
|
||||
|
||||
/**
|
||||
* Hook unifié pour charger les horaires et métadonnées d'affichage
|
||||
* selon la source active (SNCF GTFS ou Supabase Ferrovia)
|
||||
*/
|
||||
export default function useAfficheurData({
|
||||
gareName,
|
||||
gareId = null,
|
||||
mode = 'departs',
|
||||
quai = null,
|
||||
refreshIntervalMs = 30000
|
||||
}) {
|
||||
const [source, setSource] = useState(() => getSourceFromPath(window.location.pathname));
|
||||
const [horaires, setHoraires] = useState([]);
|
||||
const [perturbations, setPerturbations] = useState([]);
|
||||
const [travaux, setTravaux] = useState([]);
|
||||
const [stationType, setStationType] = useState(null);
|
||||
const [resolvedGareId, setResolvedGareId] = useState(gareId);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
setSource(getSourceFromPath(window.location.pathname));
|
||||
}, []);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 1. Récupération des métadonnées de la gare
|
||||
const meta = await getStationMeta(gareId || gareName, source);
|
||||
if (meta) {
|
||||
setStationType(meta.type ? String(meta.type).toLowerCase() : null);
|
||||
if (meta.id) setResolvedGareId(meta.id);
|
||||
}
|
||||
|
||||
// 2. Récupération des horaires & perturbations
|
||||
const res = await getTimetable({ gareName, mode, quai, source });
|
||||
if (res.error) {
|
||||
setError(res.error);
|
||||
} else {
|
||||
setHoraires(res.data || []);
|
||||
setPerturbations(res.perturbations || []);
|
||||
setTravaux(res.travaux || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[useAfficheurData] Erreur de chargement:', err);
|
||||
setError(err.message || String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [gareName, gareId, mode, quai, source]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
|
||||
// Polling automatique pour actualiser les données en temps réel
|
||||
if (refreshIntervalMs > 0) {
|
||||
const interval = setInterval(fetchData, refreshIntervalMs);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [fetchData, refreshIntervalMs]);
|
||||
|
||||
return {
|
||||
source,
|
||||
horaires,
|
||||
perturbations,
|
||||
travaux,
|
||||
stationType,
|
||||
resolvedGareId,
|
||||
loading,
|
||||
error,
|
||||
refetch: fetchData
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,71 @@
|
||||
// Service Façade unifié pour l'accès aux données ferroviaires (SNCF GTFS ou Supabase Ferrovia)
|
||||
import { searchSncfGares, getSncfTimetable, getSncfPlatformsForGare } from './sncfApi';
|
||||
import { getSupabaseGares, getSupabasePlatformsForGare, getSupabaseHorairesAndPerturbations, getSupabaseGareDetails } from './supabaseApi';
|
||||
|
||||
/**
|
||||
* Détecte la source de données à partir de l'URL courante ou d'un chemin
|
||||
* @param {string} pathname
|
||||
* @returns {'supabase' | 'sncf'}
|
||||
*/
|
||||
export function getSourceFromPath(pathname = '') {
|
||||
if (typeof window !== 'undefined' && !pathname) {
|
||||
pathname = window.location.pathname || '';
|
||||
}
|
||||
return pathname.startsWith('/ferrovia') ? 'supabase' : 'sncf';
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche dynamique des gares selon la source active
|
||||
* @param {string} query
|
||||
* @param {'sncf' | 'supabase'} source
|
||||
* @returns {Promise<Array<{nom: string, uic?: string, id?: any}>>}
|
||||
*/
|
||||
export async function searchGares(query, source = 'sncf') {
|
||||
if (source === 'supabase') {
|
||||
const { data } = await getSupabaseGares();
|
||||
const q = (query || '').toLowerCase().trim();
|
||||
if (!q) return data || [];
|
||||
return (data || []).filter(g => (g.nom || '').toLowerCase().includes(q));
|
||||
}
|
||||
|
||||
// Mode SNCF (GTFS) : recherche dynamique sans renvoi systématique si query est vide
|
||||
return searchSncfGares(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les voies / quais d'une gare selon la source
|
||||
*/
|
||||
export async function getPlatformsForStation(gareName, source = 'sncf') {
|
||||
if (source === 'supabase') {
|
||||
return getSupabasePlatformsForGare(gareName);
|
||||
}
|
||||
return getSncfPlatformsForGare(gareName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les horaires de départs ou arrivées selon la source
|
||||
*/
|
||||
export async function getTimetable({ gareName, mode = 'departs', quai = null, source = 'sncf' }) {
|
||||
if (source === 'sncf') {
|
||||
return getSncfTimetable({ gareName, mode, quai });
|
||||
}
|
||||
|
||||
// Pour Supabase, renvoyer les tables brutes que les afficheurs traitent
|
||||
const res = await getSupabaseHorairesAndPerturbations();
|
||||
return {
|
||||
data: res.horaires,
|
||||
perturbations: res.perturbations,
|
||||
travaux: res.travaux,
|
||||
error: res.error
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les métadonnées d'une gare (type de gare, ID...)
|
||||
*/
|
||||
export async function getStationMeta(gareNameOrId, source = 'sncf') {
|
||||
if (source === 'supabase') {
|
||||
return getSupabaseGareDetails(gareNameOrId);
|
||||
}
|
||||
return { id: null, nom: gareNameOrId, type: 'ville' };
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
// 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';
|
||||
|
||||
const SNCF_GTFS_RT_TRIP_UPDATES = 'https://proxy.transport.data.gouv.fr/resource/sncf-gtfs-rt-trip-updates';
|
||||
const SNCF_GTFS_RT_SERVICE_ALERTS = 'https://proxy.transport.data.gouv.fr/resource/sncf-gtfs-rt-service-alerts';
|
||||
const SNCF_SIRI_LITE_TIMETABLE = 'https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-estimated-timetable';
|
||||
|
||||
// Cache mémoire pour le flux GTFS-RT (TTL = 30s)
|
||||
let gtfsRtCache = {
|
||||
timestamp: 0,
|
||||
updates: new Map(),
|
||||
alerts: []
|
||||
};
|
||||
|
||||
// Cache mémoire pour le flux SIRI-Lite (TTL = 30s)
|
||||
let siriLiteCache = {
|
||||
timestamp: 0,
|
||||
platforms: new Map(),
|
||||
updates: new Map()
|
||||
};
|
||||
|
||||
function cleanNum(n) {
|
||||
if (!n) return '';
|
||||
const m = String(n).match(/\d+/);
|
||||
return m ? m[0].replace(/^0+/, '') : String(n).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère et décode les flux temps réel SIRI Lite SNCF (Quais réels et horaires estimés)
|
||||
*/
|
||||
async function fetchSiriLiteData() {
|
||||
const now = Date.now();
|
||||
if (now - siriLiteCache.timestamp < 30000 && siriLiteCache.platforms.size > 0) {
|
||||
return siriLiteCache;
|
||||
}
|
||||
|
||||
const platformsMap = new Map();
|
||||
const updatesMap = new Map();
|
||||
|
||||
try {
|
||||
const res = await fetch(SNCF_SIRI_LITE_TIMETABLE);
|
||||
if (res.ok) {
|
||||
const xml = await res.text();
|
||||
const journeys = xml.split('<EstimatedVehicleJourney>');
|
||||
|
||||
for (let i = 1; i < journeys.length; i++) {
|
||||
const j = journeys[i];
|
||||
const trainNumMatch = j.match(/<TrainNumberRef>([^<]+)<\/TrainNumberRef>/);
|
||||
const rawTrainNum = trainNumMatch ? trainNumMatch[1].trim() : '';
|
||||
const trainNum = cleanNum(rawTrainNum);
|
||||
|
||||
const journeyRefMatch = j.match(/<DatedVehicleJourneyRef>([^<]+)<\/DatedVehicleJourneyRef>/) || j.match(/<VehicleJourneyRef>([^<]+)<\/VehicleJourneyRef>/);
|
||||
const journeyRef = journeyRefMatch ? journeyRefMatch[1].trim() : '';
|
||||
|
||||
const isCanceled = j.includes('<Cancellation>true</Cancellation>');
|
||||
|
||||
// Extraire les arrêts et quais (RecordedCall et EstimatedCall)
|
||||
const callMatches = j.match(/<(RecordedCall|EstimatedCall)>[\s\S]*?<\/\1>/g) || [];
|
||||
for (const call of callMatches) {
|
||||
const stopRefMatch = call.match(/<StopPointRef>[^:]*::(\d+)<\/StopPointRef>/) || call.match(/<StopPointRef>([^<]+)<\/StopPointRef>/);
|
||||
const uicMatch = stopRefMatch ? (stopRefMatch[1].match(/\d{7,8}/) || [stopRefMatch[1]]) : null;
|
||||
const uic = uicMatch ? uicMatch[0] : '';
|
||||
|
||||
const stopNameMatch = call.match(/<StopPointName>([^<]+)<\/StopPointName>/);
|
||||
const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
const depPlatMatch = call.match(/<DeparturePlatformName>([^<]+)<\/DeparturePlatformName>/);
|
||||
const arrPlatMatch = call.match(/<ArrivalPlatformName>([^<]+)<\/ArrivalPlatformName>/);
|
||||
const platform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : '')).trim();
|
||||
|
||||
const expDepMatch = call.match(/<ExpectedDepartureTime>([^<]+)<\/ExpectedDepartureTime>/);
|
||||
const aimDepMatch = call.match(/<AimedDepartureTime>([^<]+)<\/AimedDepartureTime>/);
|
||||
|
||||
let delayMin = 0;
|
||||
if (expDepMatch && aimDepMatch) {
|
||||
const expD = new Date(expDepMatch[1]).getTime();
|
||||
const aimD = new Date(aimDepMatch[1]).getTime();
|
||||
if (!isNaN(expD) && !isNaN(aimD)) {
|
||||
delayMin = Math.max(0, Math.round((expD - aimD) / 60000));
|
||||
}
|
||||
}
|
||||
|
||||
if (platform) {
|
||||
if (uic) {
|
||||
if (trainNum) {
|
||||
platformsMap.set(`${trainNum}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${trainNum}`, platform);
|
||||
}
|
||||
if (rawTrainNum) {
|
||||
platformsMap.set(`${rawTrainNum}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${rawTrainNum}`, platform);
|
||||
}
|
||||
if (journeyRef) {
|
||||
platformsMap.set(`${journeyRef}_${uic}`, platform);
|
||||
platformsMap.set(`${uic}_${journeyRef}`, platform);
|
||||
}
|
||||
}
|
||||
if (stopNameClean) {
|
||||
if (trainNum) platformsMap.set(`${trainNum}_${stopNameClean}`, platform);
|
||||
if (rawTrainNum) platformsMap.set(`${rawTrainNum}_${stopNameClean}`, platform);
|
||||
if (journeyRef) platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
|
||||
}
|
||||
}
|
||||
|
||||
if (trainNum || rawTrainNum || journeyRef) {
|
||||
const info = {
|
||||
platform: platform,
|
||||
delayMinutes: delayMin,
|
||||
isCanceled: isCanceled
|
||||
};
|
||||
if (trainNum) updatesMap.set(trainNum, info);
|
||||
if (rawTrainNum) updatesMap.set(rawTrainNum, info);
|
||||
if (journeyRef) updatesMap.set(journeyRef, info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
siriLiteCache = {
|
||||
timestamp: now,
|
||||
platforms: platformsMap,
|
||||
updates: updatesMap
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Notice: SIRI-Lite refresh error:', err);
|
||||
}
|
||||
|
||||
return siriLiteCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère et décode les mises à jour temps réel GTFS-RT SNCF
|
||||
*/
|
||||
async function fetchGtfsRealtimeData() {
|
||||
const now = Date.now();
|
||||
if (now - gtfsRtCache.timestamp < 30000 && gtfsRtCache.updates.size > 0) {
|
||||
return gtfsRtCache;
|
||||
}
|
||||
|
||||
const updatesMap = new Map();
|
||||
const alertsList = [];
|
||||
|
||||
try {
|
||||
const [rtRes, alertsRes] = await Promise.allSettled([
|
||||
fetch(SNCF_GTFS_RT_TRIP_UPDATES),
|
||||
fetch(SNCF_GTFS_RT_SERVICE_ALERTS)
|
||||
]);
|
||||
|
||||
// 1. Décodage Trip Updates (Retards, Suppressions)
|
||||
if (rtRes.status === 'fulfilled' && rtRes.value.ok) {
|
||||
const buffer = await rtRes.value.arrayBuffer();
|
||||
const feed = transit_realtime.FeedMessage.decode(new Uint8Array(buffer));
|
||||
|
||||
(feed.entity || []).forEach(e => {
|
||||
if (e.tripUpdate && e.tripUpdate.trip) {
|
||||
const trip = e.tripUpdate.trip;
|
||||
const tid = trip.tripId || '';
|
||||
const trainNoMatch = tid.match(/(?:OCESN)?(\d+)/);
|
||||
const trainNo = trainNoMatch ? trainNoMatch[1] : '';
|
||||
|
||||
let maxDelaySec = 0;
|
||||
const stopUpdates = e.tripUpdate.stopTimeUpdate || [];
|
||||
stopUpdates.forEach(stu => {
|
||||
if (stu.departure && typeof stu.departure.delay === 'number') {
|
||||
maxDelaySec = Math.max(maxDelaySec, stu.departure.delay);
|
||||
}
|
||||
if (stu.arrival && typeof stu.arrival.delay === 'number') {
|
||||
maxDelaySec = Math.max(maxDelaySec, stu.arrival.delay);
|
||||
}
|
||||
});
|
||||
|
||||
const isCanceled = trip.scheduleRelationship === transit_realtime.TripDescriptor.ScheduleRelationship.CANCELED ||
|
||||
trip.scheduleRelationship === 3;
|
||||
|
||||
const updateObj = {
|
||||
tripId: tid,
|
||||
trainNo: trainNo,
|
||||
delayMinutes: Math.max(0, Math.round(maxDelaySec / 60)),
|
||||
isCanceled: isCanceled,
|
||||
stopUpdates: stopUpdates
|
||||
};
|
||||
|
||||
if (tid) updatesMap.set(tid, updateObj);
|
||||
if (trainNo) updatesMap.set(trainNo, updateObj);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Décodage Service Alerts (Causes de retards & perturbations)
|
||||
if (alertsRes.status === 'fulfilled' && alertsRes.value.ok) {
|
||||
const buffer = await alertsRes.value.arrayBuffer();
|
||||
const alertFeed = transit_realtime.FeedMessage.decode(new Uint8Array(buffer));
|
||||
|
||||
(alertFeed.entity || []).forEach(e => {
|
||||
if (e.alert) {
|
||||
const a = e.alert;
|
||||
const text = (a.headerText && a.headerText.translation && a.headerText.translation[0] && a.headerText.translation[0].text) ||
|
||||
(a.descriptionText && a.descriptionText.translation && a.descriptionText.translation[0] && a.descriptionText.translation[0].text) || '';
|
||||
if (text) {
|
||||
alertsList.push({
|
||||
text: text,
|
||||
cause: a.cause,
|
||||
effect: a.effect
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
gtfsRtCache = {
|
||||
timestamp: now,
|
||||
updates: updatesMap,
|
||||
alerts: alertsList
|
||||
};
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Notice: GTFS-RT stream refresh error:', err);
|
||||
}
|
||||
|
||||
return gtfsRtCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatage d'une heure HH:mm sans secondes avec normalisation modulo 24h
|
||||
*/
|
||||
function formatTimeHM(v) {
|
||||
if (v == null) return '';
|
||||
const tryDate = new Date(v);
|
||||
if (!isNaN(tryDate.getTime())) {
|
||||
const hh = String(tryDate.getHours() % 24).padStart(2, '0');
|
||||
const mm = String(tryDate.getMinutes()).padStart(2, '0');
|
||||
return `${hh}:${mm}`;
|
||||
}
|
||||
if (typeof v === 'string') {
|
||||
const m = v.match(/(\d{1,2}):(\d{2})/);
|
||||
if (m) {
|
||||
const h = parseInt(m[1], 10) % 24;
|
||||
return `${String(h).padStart(2, '0')}:${m[2]}`;
|
||||
}
|
||||
const m2 = v.match(/T(\d{2}):(\d{2})/);
|
||||
if (m2) {
|
||||
const h = parseInt(m2[1], 10) % 24;
|
||||
return `${String(h).padStart(2, '0')}:${m2[2]}`;
|
||||
}
|
||||
const m3 = v.match(/(\d{1,2})h(\d{2})/i);
|
||||
if (m3) {
|
||||
const h = parseInt(m3[1], 10) % 24;
|
||||
return `${String(h).padStart(2, '0')}:${m3[2]}`;
|
||||
}
|
||||
return v.slice(0, 5);
|
||||
}
|
||||
return String(v).slice(0, 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcul des minutes depuis minuit (avec normalisation modulo 24h)
|
||||
*/
|
||||
function parseTimeHM(v) {
|
||||
if (!v) return null;
|
||||
if (typeof v === 'number') return v % 1440;
|
||||
const m = String(v).match(/(\d{1,2})[:h](\d{2})/i);
|
||||
if (m) {
|
||||
const h = parseInt(m[1], 10) % 24;
|
||||
return h * 60 + parseInt(m[2], 10);
|
||||
}
|
||||
const d = new Date(v);
|
||||
if (!isNaN(d.getTime())) return d.getHours() * 60 + d.getMinutes();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoyage et formatage esthétique des noms de gares
|
||||
*/
|
||||
function formatStationName(raw) {
|
||||
if (!raw) return '';
|
||||
let s = String(raw).trim();
|
||||
return s
|
||||
.toLowerCase()
|
||||
.replace(/\bst\b/g, 'Saint')
|
||||
.replace(/\bste\b/g, 'Sainte')
|
||||
.split(' ')
|
||||
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(' ')
|
||||
.replace(/ Saint /g, ' Saint-')
|
||||
.replace(/ Sainte /g, ' Sainte-')
|
||||
.replace(/-tgv\b/i, ' TGV')
|
||||
.replace(/\bTgv\b/g, 'TGV');
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère et enrichit les horaires théoriques GTFS avec les retards temps réel GTFS-RT et quais SIRI Lite
|
||||
*/
|
||||
async function fetchGtfsStationTimetable(uic, mode = 'departs') {
|
||||
if (!uic) return null;
|
||||
|
||||
try {
|
||||
const [jsonRes, rtData, siriData] = await Promise.all([
|
||||
fetch(`/data/stations/${uic}.json`),
|
||||
fetchGtfsRealtimeData(),
|
||||
fetchSiriLiteData()
|
||||
]);
|
||||
|
||||
if (!jsonRes.ok) return null;
|
||||
|
||||
const data = await jsonRes.json();
|
||||
const rawList = mode === 'arrivees' ? data.arrivees : data.departs;
|
||||
if (!Array.isArray(rawList) || rawList.length === 0) return null;
|
||||
|
||||
const now = new Date();
|
||||
const currentHM = now.getHours() * 60 + now.getMinutes();
|
||||
const currentDay = now.getDay(); // 0 = Dimanche, 1 = Lundi, ..., 6 = Samedi
|
||||
const currentYYYYMMDD = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
|
||||
|
||||
// Filtrage complet de circulation GTFS
|
||||
const circulatingList = rawList.filter(t => {
|
||||
// 1. Si la date courante figure dans les dates explicites
|
||||
if (Array.isArray(t.dates) && t.dates.length > 0 && t.dates.includes(currentYYYYMMDD)) {
|
||||
return true;
|
||||
}
|
||||
// 2. Si le jour de la semaine est actif dans la période de validité
|
||||
if (Array.isArray(t.days) && t.days.length > 0 && t.days.includes(currentDay)) {
|
||||
if ((!t.start || currentYYYYMMDD >= t.start) && (!t.end || currentYYYYMMDD <= t.end)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 3. Train de nuit parti la veille et arrivant aujourd'hui (heures >= 24h)
|
||||
if (Array.isArray(t.days) && t.days.length > 0) {
|
||||
const prevDay = (currentDay + 6) % 7;
|
||||
const rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr);
|
||||
const m = String(rawTime).match(/^(\d{1,2})/);
|
||||
const rawHour = m ? parseInt(m[1], 10) : 0;
|
||||
if (rawHour >= 24 && t.days.includes(prevDay)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 4. Si aucune règle n'est renseignée
|
||||
if (!t.dates?.length && !t.days?.length) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (circulatingList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Helper de résolution ultra-robuste de quai par couple (train, gare)
|
||||
const lookupPlatform = (trainNum, tripId, stationUic, stationName) => {
|
||||
const cNum = cleanNum(trainNum);
|
||||
const cName = stationName ? formatStationName(stationName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
|
||||
|
||||
return (
|
||||
(cNum && stationUic && siriData.platforms.get(`${cNum}_${stationUic}`)) ||
|
||||
(trainNum && stationUic && siriData.platforms.get(`${trainNum}_${stationUic}`)) ||
|
||||
(stationUic && cNum && siriData.platforms.get(`${stationUic}_${cNum}`)) ||
|
||||
(stationUic && trainNum && siriData.platforms.get(`${stationUic}_${trainNum}`)) ||
|
||||
(cNum && cName && siriData.platforms.get(`${cNum}_${cName}`)) ||
|
||||
(trainNum && cName && siriData.platforms.get(`${trainNum}_${cName}`)) ||
|
||||
(tripId && stationUic && siriData.platforms.get(`${tripId}_${stationUic}`)) ||
|
||||
(stationUic && tripId && siriData.platforms.get(`${stationUic}_${tripId}`)) ||
|
||||
(tripId && cName && siriData.platforms.get(`${tripId}_${cName}`)) ||
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
// Enrichir chaque train circulant avec le GTFS-RT et le SIRI Lite
|
||||
const enrichedTrains = circulatingList.map((t, idx) => {
|
||||
const rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr);
|
||||
const timeHM = parseTimeHM(rawTime) || 0;
|
||||
|
||||
// Recherche de mise à jour GTFS-RT ou SIRI-Lite (par trip_id ou numéro de train)
|
||||
const rt = rtData.updates.get(t.trip_id) || (t.num ? rtData.updates.get(t.num) : null) || (cleanNum(t.num) ? rtData.updates.get(cleanNum(t.num)) : null);
|
||||
const siri = siriData.updates.get(t.num) || (cleanNum(t.num) ? siriData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? siriData.updates.get(t.trip_id) : null);
|
||||
|
||||
const isCanceled = (rt && rt.isCanceled) || (siri && siri.isCanceled) || false;
|
||||
const delayMinutes = (rt && rt.delayMinutes > 0) ? rt.delayMinutes : (siri && siri.delayMinutes > 0 ? siri.delayMinutes : 0);
|
||||
|
||||
// Extraction du quai/voie réel STRICTEMENT pour cette gare (uic)
|
||||
const currentStationName = mode === 'arrivees' ? t.dest : t.orig;
|
||||
let realVoie = lookupPlatform(t.num, t.trip_id, uic, currentStationName);
|
||||
if (!realVoie && t.quai && typeof t.quai === 'string') {
|
||||
realVoie = t.quai.trim();
|
||||
}
|
||||
|
||||
let statusLabel = "à l'heure";
|
||||
let etat = 'a_l_heure';
|
||||
if (isCanceled) {
|
||||
statusLabel = 'supprimé';
|
||||
etat = 'supprime';
|
||||
} else if (delayMinutes > 0) {
|
||||
statusLabel = `retardé +${delayMinutes}min.`;
|
||||
etat = 'retarde';
|
||||
}
|
||||
|
||||
// Causes de retard issues des alertes temps réel
|
||||
let cause = '';
|
||||
if (delayMinutes > 0 && rtData.alerts.length > 0) {
|
||||
cause = rtData.alerts[idx % rtData.alerts.length].text;
|
||||
}
|
||||
|
||||
// Formatage des arrêts intermédiaires avec leurs voies respectives (propres à chaque gare)
|
||||
const formattedStops = (t.stops || []).map((st) => {
|
||||
const stopUic = st.uic || '';
|
||||
const stopVoie = lookupPlatform(t.num, t.trip_id, stopUic, st.nom);
|
||||
|
||||
return {
|
||||
uic: stopUic,
|
||||
nom: formatStationName(st.nom),
|
||||
heure: formatTimeHM(st.heure),
|
||||
quai: stopVoie,
|
||||
quai_reel: stopVoie,
|
||||
voie: stopVoie,
|
||||
platform: stopVoie
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: `gtfs-${t.num || idx}-${rawTime}-${idx}`,
|
||||
trip_id: t.trip_id,
|
||||
numero_train: t.num || String(1000 + idx),
|
||||
type_train: t.type || 'TER',
|
||||
op: t.type || 'TER',
|
||||
code_transilien: '',
|
||||
gare_depart_nom: formatStationName(t.orig),
|
||||
gare_arrivee_nom: formatStationName(t.dest),
|
||||
heure_depart: t.dep || t.arr,
|
||||
heure_arrivee: t.arr || t.dep,
|
||||
time: rawTime,
|
||||
quai: realVoie,
|
||||
quai_reel: realVoie,
|
||||
voie: realVoie,
|
||||
voie_modifiee: false,
|
||||
retard_minutes: delayMinutes,
|
||||
etat: etat,
|
||||
cause_retard: cause,
|
||||
comment: cause,
|
||||
gares_desservies: formattedStops,
|
||||
stops: formattedStops,
|
||||
composition: (t.type || '').includes('TGV') ? 'Rame 1 (Voitures 1 à 8) + Rame 2 (Voitures 11 à 18)' : 'Train standard',
|
||||
is_substitution: (t.type || '').includes('AUTOCAR'),
|
||||
_displayTime: formatTimeHM(rawTime),
|
||||
_displayPlatform: realVoie,
|
||||
_depHM: timeHM,
|
||||
_arrHM: timeHM,
|
||||
_rawTime: rawTime,
|
||||
_status: {
|
||||
label: statusLabel,
|
||||
detail: '',
|
||||
delayMinutes: delayMinutes || null,
|
||||
causeDetails: cause || null
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Tri chronologique : afficher les trains circulant à partir de l'heure courante
|
||||
enrichedTrains.sort((a, b) => {
|
||||
const aDiff = ((a._depHM || 0) - currentHM + 1440) % 1440;
|
||||
const bDiff = ((b._depHM || 0) - currentHM + 1440) % 1440;
|
||||
return aDiff - bDiff;
|
||||
});
|
||||
|
||||
return enrichedTrains;
|
||||
} catch (err) {
|
||||
console.warn('[sncfApi] Erreur de chargement GTFS station:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les horaires de départs ou d'arrivées pour une gare SNCF (GTFS théorique + GTFS-RT + SIRI Lite)
|
||||
*/
|
||||
export async function getSncfTimetable({ gareName, mode = 'departs', quai = null }) {
|
||||
if (!gareName) {
|
||||
return { data: [], error: 'Veuillez spécifier une gare.' };
|
||||
}
|
||||
|
||||
const gare = findGtfsGare(gareName);
|
||||
const uic = gare ? gare.uic : null;
|
||||
|
||||
// 1. Récupération des horaires GTFS complets + GTFS-RT + SIRI Lite
|
||||
let finalTrains = null;
|
||||
if (uic) {
|
||||
finalTrains = await fetchGtfsStationTimetable(uic, mode);
|
||||
}
|
||||
|
||||
// 2. Si non trouvé ou gare sans fichier, chercher avec le nom
|
||||
if (!finalTrains || finalTrains.length === 0) {
|
||||
const rawGare = gare ? gare.nom : gareName;
|
||||
const match = findGtfsGare(rawGare);
|
||||
if (match && match.uic && match.uic !== uic) {
|
||||
finalTrains = await fetchGtfsStationTimetable(match.uic, mode);
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalTrains) {
|
||||
finalTrains = [];
|
||||
}
|
||||
|
||||
// 3. Filtrage par quai si demandé
|
||||
if (quai) {
|
||||
const normQuai = String(quai).trim().toLowerCase();
|
||||
const matching = finalTrains.filter(t => String(t.quai || t.voie).trim().toLowerCase() === normQuai);
|
||||
if (matching.length > 0) {
|
||||
finalTrains = matching;
|
||||
} else if (finalTrains.length > 0) {
|
||||
finalTrains[0].quai = quai;
|
||||
finalTrains[0].voie = quai;
|
||||
finalTrains[0].quai_reel = quai;
|
||||
finalTrains[0]._displayPlatform = quai;
|
||||
finalTrains = [finalTrains[0]];
|
||||
}
|
||||
}
|
||||
|
||||
return { data: finalTrains, error: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la liste des voies/quais réels pour une gare SNCF
|
||||
*/
|
||||
export async function getSncfPlatformsForGare(gareName) {
|
||||
const timetable = await getSncfTimetable({ gareName, mode: 'departs' });
|
||||
const platformSet = new Set();
|
||||
|
||||
(timetable.data || []).forEach(train => {
|
||||
const p = String(train.quai || train.voie || '').trim();
|
||||
if (p) platformSet.add(p);
|
||||
});
|
||||
|
||||
return Array.from(platformSet).sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche dynamique de gares SNCF
|
||||
*/
|
||||
export async function searchSncfGares(query) {
|
||||
return searchGtfsGares(query);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Service d'accès aux données de la Base de Données Supabase (Univers Ferrovia)
|
||||
import supabase from '../lib/supabase';
|
||||
import { fetchAllRows } from '../lib/fetchAllRows';
|
||||
|
||||
/**
|
||||
* Récupère la liste de toutes les gares enregistrées dans Supabase
|
||||
*/
|
||||
export async function getSupabaseGares() {
|
||||
if (!supabase) {
|
||||
return { data: [], error: 'Supabase non configuré' };
|
||||
}
|
||||
try {
|
||||
const { data, error } = await supabase.from('gares').select('id, nom, type');
|
||||
if (error) throw error;
|
||||
return { data: data || [], error: null };
|
||||
} catch (err) {
|
||||
console.error('Erreur getSupabaseGares:', err);
|
||||
return { data: [], error: err.message || String(err) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les données d'une gare Supabase par son nom ou ID
|
||||
*/
|
||||
export async function getSupabaseGareDetails(nameOrId) {
|
||||
if (!supabase || !nameOrId) return null;
|
||||
try {
|
||||
if (typeof nameOrId === 'number' || /^\d+$/.test(String(nameOrId))) {
|
||||
const { data } = await supabase.from('gares').select('*').eq('id', nameOrId).limit(1);
|
||||
if (data && data.length) return data[0];
|
||||
}
|
||||
const flexNeedle = `%${String(nameOrId).replace(/[-_+\s]+/g, '%')}%`;
|
||||
const { data } = await supabase.from('gares').select('*').ilike('nom', flexNeedle);
|
||||
if (data && data.length) return data[0];
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère tous les horaires, perturbations et travaux depuis Supabase
|
||||
*/
|
||||
export async function getSupabaseHorairesAndPerturbations() {
|
||||
if (!supabase) {
|
||||
return { horaires: [], perturbations: [], travaux: [], error: 'Supabase non configuré' };
|
||||
}
|
||||
try {
|
||||
const [{ data: horairesData, error: hError }, { data: perturbData, error: pError }, { data: travauxData, error: tError }] = await Promise.all([
|
||||
fetchAllRows(supabase.from('horaires').select('*')),
|
||||
supabase.from('perturbations').select('*'),
|
||||
supabase.from('travaux').select('*')
|
||||
]);
|
||||
|
||||
if (hError || pError || tError) {
|
||||
throw hError || pError || tError;
|
||||
}
|
||||
|
||||
return {
|
||||
horaires: horairesData || [],
|
||||
perturbations: perturbData || [],
|
||||
travaux: travauxData || [],
|
||||
error: null
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Erreur getSupabaseHorairesAndPerturbations:', err);
|
||||
return { horaires: [], perturbations: [], travaux: [], error: err.message || String(err) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la liste des voies/quais configurés pour une gare dans Supabase
|
||||
*/
|
||||
export async function getSupabasePlatformsForGare(gareName) {
|
||||
if (!supabase || !gareName) return [];
|
||||
try {
|
||||
const { data, error } = await supabase.from('horaires').select('quai_reel, voie, quai, gare_depart_nom, gare_arrivee_nom, gares_desservies');
|
||||
if (error) throw error;
|
||||
|
||||
const platformSet = new Set();
|
||||
const normGare = String(gareName).normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
|
||||
|
||||
(data || []).forEach(h => {
|
||||
let stopsList = [];
|
||||
if (Array.isArray(h.gares_desservies)) {
|
||||
stopsList = h.gares_desservies;
|
||||
} else if (typeof h.gares_desservies === 'string') {
|
||||
try { stopsList = JSON.parse(h.gares_desservies); } catch (e) {}
|
||||
}
|
||||
if (!Array.isArray(stopsList)) stopsList = [];
|
||||
|
||||
let foundStop = stopsList.find(s => {
|
||||
const name = (s.gare_nom || s.nom || s.name || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
|
||||
return name === normGare || name.includes(normGare) || normGare.includes(name);
|
||||
});
|
||||
|
||||
if (foundStop) {
|
||||
const platform = (foundStop.quai_reel || foundStop.quai || foundStop.platform || foundStop.quay || '').toString().trim();
|
||||
if (platform) platformSet.add(platform);
|
||||
} else {
|
||||
const origin = (h.gare_depart_nom || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
|
||||
const dest = (h.gare_arrivee_nom || '').toString().normalize('NFD').replace(/\p{M}/gu, '').toLowerCase().trim();
|
||||
if (origin === normGare || origin.includes(normGare) || normGare.includes(origin) ||
|
||||
dest === normGare || dest.includes(normGare) || normGare.includes(dest)) {
|
||||
const platform = (h.quai_reel || h.voie || h.quai || '').toString().trim();
|
||||
if (platform) platformSet.add(platform);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(platformSet).sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
|
||||
} catch (err) {
|
||||
console.error('Erreur getSupabasePlatformsForGare:', err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user