Files
ferrovia-infogare/src/services/sncfApi.js
T
mathis d8f8ceaa9f RELEASE V5.6.0 : Voici les modifications apportées :
-> REFONTE TOTALE DE TOUS LES AFFICHEURS CLASSIQUES + QUAIS + OPTION TGV

FONCTIONNALITÉS EN COURS DE DÉVELOPPEMENT :

-> ADAPTABILITÉ EN FONCTION DES SITUATIONS
2026-08-31 17:32:58 +02:00

1097 lines
45 KiB
JavaScript

// 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';
const SNCF_SIRI_LITE_SITUATION_EXCHANGE = 'https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-situation-exchange';
const NAVITIA_BASE_URL = 'https://api.sncf.com/v1';
const NAVITIA_TOKEN = (typeof process !== 'undefined' && process.env && process.env.REACT_APP_NAVITIA_TOKEN) || 'bf06df97-2ff1-4560-b74c-f127cb70764d';
// TTL de rafraîchissement des flux temps réel SNCF : 2 minutes (120 secondes)
const REALTIME_CACHE_TTL = 120000;
// Cache mémoire pour le flux GTFS-RT (TTL = 2 min)
let gtfsRtCache = {
timestamp: 0,
updates: new Map(),
alerts: []
};
// Cache mémoire pour le flux SIRI-Lite Estimated Timetable (TTL = 2 min)
let siriLiteCache = {
timestamp: 0,
platforms: new Map(),
updates: new Map()
};
// Cache mémoire pour le flux SIRI-Lite Situation Exchange (TTL = 2 min)
let situationExchangeCache = {
timestamp: 0,
platforms: new Map(),
updates: new Map(),
alerts: []
};
// Cache mémoire pour l'API OpenData SNCF (TTL = 2 min)
const openDataPlatformsCache = new Map();
const MAJOR_HUBS = [
'paris', 'lyon', 'dijon', 'marseille', 'lille', 'strasbourg', 'bordeaux',
'toulouse', 'nantes', 'rennes', 'nice', 'montpellier', 'nimes', 'metz',
'nancy', 'mulhouse', 'tours', 'le mans', 'rouen', 'clermont ferrand',
'grenoble', 'chambery', 'avignon', 'valence', 'besancon viotte',
'saint etienne', 'toulon', 'cannes', 'antibes', 'angers'
];
const MAJOR_HUB_UICS = [
'87713040', '87686006', '87271007', '87391003', '87113001', '87384008',
'87547000', '87686667', '87722025', '87723197', '87751008', '87286005',
'87286013', '87212027', '87581009', '87611004', '87481002', '87471003',
'87756056', '87773002', '87775007', '87192039', '87141002', '87182063',
'87571000', '87444000', '87411017', '87734004', '87747006', '87741009',
'87765008', '87318964', '87763029', '87718007', '87726000', '87755009',
'87757625', '87757674', '87484006'
];
export function isMajorHub(stationName, uic = '') {
if (uic && MAJOR_HUB_UICS.includes(String(uic).replace(/\D/g, ''))) return true;
if (!stationName) return false;
const clean = String(stationName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim();
return MAJOR_HUBS.some(hub => clean === hub || clean.startsWith(hub + ' ') || clean.endsWith(' ' + hub));
}
export function isCarNumber(num) {
const d = String(num || '').replace(/\D/g, '');
return (d.length === 5 && d.startsWith('3')) || (d.length === 6 && d.startsWith('3'));
}
/**
* Résout la voie pour une circulation :
* - Aucune voie nominale/fictive inventée : seuls les quais temps réel SNCF sont affichés.
* - Les services par autocar affichent 'AUTOCAR' (pictogramme).
*/
export function resolveNominalTrack(stationName, trainNum, uic = '') {
const rawNum = String(trainNum || '').replace(/\D/g, '');
if (isCarNumber(rawNum)) return 'AUTOCAR';
return '';
}
// Helper fetch avec timeout pour ne jamais bloquer l'affichage en cas de réseau lent ou de blocage
async function fetchWithTimeout(url, options = {}, timeoutMs = 25000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, { ...options, signal: controller.signal });
clearTimeout(id);
return res;
} catch (err) {
clearTimeout(id);
throw err;
}
}
function cleanNum(n) {
if (!n) return '';
const m = String(n).match(/\d+/);
return m ? m[0].replace(/^0+/, '') : String(n).trim();
}
function extractAllTrainNumbers(journeyXml) {
const nums = new Set();
// 1. TrainNumberRef
const tnr = journeyXml.match(/<TrainNumberRef>([^<]+)<\/TrainNumberRef>/);
if (tnr) {
const c = cleanNum(tnr[1]);
if (c) nums.add(c);
}
// 2. VehicleJourneyName
const vjn = journeyXml.match(/<VehicleJourneyName>([^<]+)<\/VehicleJourneyName>/);
if (vjn) {
const c = cleanNum(vjn[1]);
if (c) nums.add(c);
}
// 3. PublishedLineName if numeric
const pln = journeyXml.match(/<PublishedLineName>([^<]+)<\/PublishedLineName>/);
if (pln) {
const m = pln[1].match(/^\d+$/);
if (m) nums.add(cleanNum(m[0]));
}
// 4. DatedVehicleJourneyRef / VehicleJourneyRef / FramedVehicleJourneyRef
const refs = journeyXml.match(/<(?:DatedVehicleJourneyRef|VehicleJourneyRef|FramedVehicleJourneyRef)>([\s\S]*?)<\/(?:DatedVehicleJourneyRef|VehicleJourneyRef|FramedVehicleJourneyRef)>/g) || [];
for (const r of refs) {
const vjMatch = r.match(/FR:VehicleJourney::(\d{3,7})/i) || r.match(/VehicleJourney::(\d{3,7})/i) || r.match(/::(\d{3,7})/);
if (vjMatch) {
nums.add(cleanNum(vjMatch[1]));
}
const lrMatch = r.match(/LineRef::(\d{3,7})/i);
if (lrMatch) {
nums.add(cleanNum(lrMatch[1]));
}
}
return Array.from(nums);
}
/**
* Récupère et décode le flux SIRI Lite SNCF officiel en temps réel (circulations dans les 60 prochaines minutes)
* Source : https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-estimated-timetable
*/
async function fetchSiriLiteData() {
const now = Date.now();
if (now - siriLiteCache.timestamp < REALTIME_CACHE_TTL && siriLiteCache.platforms.size > 0) {
return siriLiteCache;
}
const platformsMap = new Map();
const updatesMap = new Map();
try {
const res = await fetchWithTimeout(SNCF_SIRI_LITE_TIMETABLE, {
headers: {
'Accept': 'application/xml, text/xml, */*'
}
}, 25000);
if (res && res.ok) {
const xml = await res.text();
const journeys = xml.split('<EstimatedVehicleJourney>');
for (let i = 1; i < journeys.length; i++) {
const j = journeys[i];
// 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) {
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);
}
}
if (journeyRef) {
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);
}
}
}
}
siriLiteCache = {
timestamp: now,
platforms: platformsMap,
updates: updatesMap
};
} catch (err) {
console.warn('[sncfApi] Notice: SIRI-Lite Estimated Timetable refresh error/timeout:', err);
}
return siriLiteCache;
}
/**
* Récupère et décode le flux SIRI Lite Situation Exchange SNCF (Circulations à venir, perturbations, changements de voie, suppressions)
* Source : https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-situation-exchange
*/
async function fetchSituationExchangeData() {
const now = Date.now();
if (now - situationExchangeCache.timestamp < REALTIME_CACHE_TTL && situationExchangeCache.platforms.size > 0) {
return situationExchangeCache;
}
const platformsMap = new Map();
const updatesMap = new Map();
const alertsList = [];
try {
const res = await fetchWithTimeout(SNCF_SIRI_LITE_SITUATION_EXCHANGE, {
headers: {
'Accept': 'application/xml, text/xml, */*'
}
}, 12000);
if (res && res.ok) {
const xml = await res.text();
const situations = xml.split('<PtSituationElement>');
for (let i = 1; i < situations.length; i++) {
const s = situations[i];
// 1. Extraction exhaustive du train et de la mission
const trainNums = extractAllTrainNumbers(s);
const trainNumMatch = s.match(/<TrainNumberRef>([^<]+)<\/TrainNumberRef>/) || s.match(/<VehicleJourneyRef>([^<]+)<\/VehicleJourneyRef>/) || s.match(/<PublishedLineName>([^<]+)<\/PublishedLineName>/);
const rawTrainNum = trainNumMatch ? trainNumMatch[1].trim() : '';
const primaryTrainNum = cleanNum(rawTrainNum);
if (primaryTrainNum && !trainNums.includes(primaryTrainNum)) {
trainNums.push(primaryTrainNum);
}
// 2. Extraction de l'arrêt impacté
const stopRefMatch = s.match(/<StopPointRef>([^<]+)<\/StopPointRef>/);
let uic = '';
let uic7 = '';
if (stopRefMatch) {
const rawRef = stopRefMatch[1];
const uicDigits = rawRef.match(/\d{7,8}/);
uic = uicDigits ? uicDigits[0] : rawRef.replace(/\D/g, '');
if (uic.length === 8 && uic.startsWith('87')) {
uic7 = uic.slice(2);
}
}
const stopNameMatch = s.match(/<StopPointName>([^<]+)<\/StopPointName>/);
const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
// 3. Extraction des messages en français
const summaryFrMatch = s.match(/<Summary[^>]*xml:lang="FR"[^>]*>([^<]+)<\/Summary>/i) || s.match(/<Summary[^>]*>([^<]+)<\/Summary>/i);
const summary = summaryFrMatch ? summaryFrMatch[1].trim() : '';
const descFrMatch = s.match(/<Description[^>]*xml:lang="FR"[^>]*>([^<]+)<\/Description>/i) || s.match(/<Description[^>]*>([^<]+)<\/Description>/i);
const desc = descFrMatch ? descFrMatch[1].trim() : '';
const messageText = desc || summary || '';
// 4. Détection de changement de quai / voie
const platTagMatch = s.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName|ArrivalPlatformName|PlannedArrivalPlatformName|QuayRef)>([^<]+)<\//i);
let platform = platTagMatch ? platTagMatch[1].replace(/^(?:voie|quai|track|platform)\s*/i, '').trim() : '';
if (!platform && messageText) {
const voieTextMatch = messageText.match(/\b(?:voie|quai)\s+([A-Za-z0-9]+)\b/i);
if (voieTextMatch) {
platform = voieTextMatch[1].trim();
}
}
// 5. Détection de suppression
const isCanceled = /supprim|annul|gelöscht|cancelled|interrompu/i.test(messageText) || s.includes('<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);
}
}
}
const info = {
platform: platform,
delayMinutes: 0,
isCanceled: isCanceled,
cause: summary || desc || ''
};
for (const tNum of trainNums) {
if (uic) updatesMap.set(`${tNum}_${uic}`, info);
updatesMap.set(tNum, info);
}
if (messageText) {
alertsList.push({
header: summary,
text: desc || summary
});
}
}
}
situationExchangeCache = {
timestamp: now,
platforms: platformsMap,
updates: updatesMap,
alerts: alertsList
};
} catch (err) {
console.warn('[sncfApi] Notice: SIRI-Lite Situation Exchange error/timeout:', err);
}
return situationExchangeCache;
}
/**
* Récupère les quais en temps réel depuis l'API OpenData SNCF (Navitia + OpenDataSoft)
*/
async function fetchOpenDataSncfPlatforms(uic, mode = 'departs') {
if (!uic) return new Map();
const cacheKey = `${uic}_${mode}`;
const now = Date.now();
if (openDataPlatformsCache.has(cacheKey)) {
const cached = openDataPlatformsCache.get(cacheKey);
if (now - cached.timestamp < 30000) {
return cached.platforms;
}
}
const platformsMap = new Map();
try {
const authHeader = NAVITIA_TOKEN.startsWith('Basic') ? NAVITIA_TOKEN : 'Basic ' + btoa(NAVITIA_TOKEN + ':');
const uicClean = String(uic).replace(/\D/g, '');
const isArrival = mode === 'arrivees';
const navitiaPath = isArrival ? 'arrivals' : 'departures';
const odsDataset = isArrival ? 'prochaines-arrivees-des-trains-en-gare-temps-reel' : 'prochains-departs-des-trains-en-gare-temps-reel';
const navitiaEndpoints = [
`https://api.sncf.com/v1/coverage/sncf/stop_areas/stop_area:OCE:SA:${uicClean}/${navitiaPath}?count=100`,
`https://api.sncf.com/v1/coverage/sncf/stop_areas/stop_area:SNCF:${uicClean}/${navitiaPath}?count=100`,
`https://api.sncf.com/v1/coverage/sncf/stop_points/stop_point:OCE:SP:${uicClean}/${navitiaPath}?count=100`,
`https://api.navitia.io/v1/coverage/sncf/stop_areas/stop_area:OCE:SA:${uicClean}/${navitiaPath}?count=100`,
`https://api.navitia.io/v1/coverage/sncf/stop_areas/stop_area:${uicClean}/${navitiaPath}?count=100`
];
const [navitiaRes, odsV2Res, odsV1Res] = await Promise.allSettled([
fetchWithTimeout(navitiaEndpoints[0], { headers: { 'Authorization': authHeader } }, 5000)
.then(async res => {
if (res && res.ok) return res;
return fetchWithTimeout(navitiaEndpoints[1], { headers: { 'Authorization': authHeader } }, 5000);
})
.then(async res => {
if (res && res.ok) return res;
return fetchWithTimeout(navitiaEndpoints[3], { headers: { 'Authorization': NAVITIA_TOKEN } }, 5000);
}),
fetchWithTimeout(`https://ressources.data.sncf.com/api/explore/v2.1/catalog/datasets/${odsDataset}/records?limit=100&where=search(code_uic%2C%22${uicClean}%22)`, {}, 5000),
fetchWithTimeout(`https://data.sncf.com/api/records/1.0/search/?dataset=${odsDataset}&q=${uicClean}&rows=100`, {}, 5000)
]);
// 1. Décodage Navitia (API OpenData SNCF temps réel)
if (navitiaRes.status === 'fulfilled' && navitiaRes.value && navitiaRes.value.ok) {
const data = await navitiaRes.value.json();
const list = (isArrival ? data.arrivals : data.departures) || [];
for (const item of list) {
const info = item.display_informations || {};
const trainNum = cleanNum(info.headsign || info.trip_short_name || info.commercial_mode || info.code || '');
const rawNum = String(info.headsign || info.trip_short_name || info.code || '').replace(/\D/g, '');
const stopPoint = item.stop_point || (item.stop_date_time && item.stop_date_time.stop_point) || {};
let platform = (
stopPoint.platform_code ||
info.platform ||
''
).toString().trim();
if (!platform && stopPoint.label) {
const m = stopPoint.label.match(/(?:voie|quai|track|platform)\s*([a-z0-9]+)/i);
if (m) platform = m[1];
}
platform = platform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
const rawDt = (item.stop_date_time && (item.stop_date_time.departure_date_time || item.stop_date_time.arrival_date_time || item.stop_date_time.base_departure_date_time || item.stop_date_time.base_arrival_date_time)) || '';
let timeHM = '';
if (rawDt.includes('T')) {
const tPart = rawDt.split('T')[1];
if (tPart && tPart.length >= 4) {
timeHM = `${tPart.slice(0, 2)}:${tPart.slice(2, 4)}`;
}
}
const destClean = info.direction ? formatStationName(info.direction).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
if (platform) {
if (trainNum && uicClean) {
platformsMap.set(`${trainNum}_${uicClean}`, platform);
platformsMap.set(`${uicClean}_${trainNum}`, platform);
}
if (rawNum && uicClean) {
platformsMap.set(`${rawNum}_${uicClean}`, platform);
platformsMap.set(`${uicClean}_${rawNum}`, platform);
}
if (trainNum && destClean) {
platformsMap.set(`${trainNum}_${destClean}`, platform);
}
}
}
}
// 2. Décodage OpenDataSoft SNCF v2.1
if (odsV2Res.status === 'fulfilled' && odsV2Res.value && odsV2Res.value.ok) {
const data = await odsV2Res.value.json();
const records = data.results || data.records || [];
for (const r of records) {
const fields = r.fields || r;
const trainNum = cleanNum(fields.num || fields.numero || fields.train || fields.num_train || '');
const rawNum = String(fields.num || fields.numero || fields.train || fields.num_train || '').replace(/\D/g, '');
const platform = String(fields.voie || fields.quai || fields.platform || fields.track || fields.voie_quai || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
const destClean = fields.destination || fields.orig ? formatStationName(fields.destination || fields.orig).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
if (platform) {
if (trainNum && uicClean) {
platformsMap.set(`${trainNum}_${uicClean}`, platform);
platformsMap.set(`${uicClean}_${trainNum}`, platform);
}
if (rawNum && uicClean) {
platformsMap.set(`${rawNum}_${uicClean}`, platform);
platformsMap.set(`${uicClean}_${rawNum}`, platform);
}
if (trainNum && destClean) {
platformsMap.set(`${trainNum}_${destClean}`, platform);
}
}
}
}
// 3. Décodage OpenDataSoft SNCF v1.0
if (odsV1Res.status === 'fulfilled' && odsV1Res.value && odsV1Res.value.ok) {
const data = await odsV1Res.value.json();
const records = data.records || [];
for (const r of records) {
const fields = r.fields || {};
const trainNum = cleanNum(fields.num || fields.numero || fields.train || fields.num_train || '');
const rawNum = String(fields.num || fields.numero || fields.train || fields.num_train || '').replace(/\D/g, '');
const platform = String(fields.voie || fields.quai || fields.platform || fields.track || fields.voie_quai || '').replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
const destClean = fields.destination || fields.orig ? formatStationName(fields.destination || fields.orig).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
if (platform) {
if (trainNum && uicClean) {
platformsMap.set(`${trainNum}_${uicClean}`, platform);
platformsMap.set(`${uicClean}_${trainNum}`, platform);
}
if (rawNum && uicClean) {
platformsMap.set(`${rawNum}_${uicClean}`, platform);
platformsMap.set(`${uicClean}_${rawNum}`, platform);
}
if (trainNum && destClean) {
platformsMap.set(`${trainNum}_${destClean}`, platform);
}
}
}
}
} catch (err) {
console.warn('[sncfApi] Notice: OpenData SNCF platform fetch error:', err);
}
openDataPlatformsCache.set(cacheKey, {
timestamp: now,
platforms: platformsMap
});
return platformsMap;
}
/**
* Récupère et décode les mises à jour temps réel GTFS-RT SNCF
*/
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([
fetchWithTimeout(SNCF_GTFS_RT_TRIP_UPDATES, {}, 6000),
fetchWithTimeout(SNCF_GTFS_RT_SERVICE_ALERTS, {}, 6000)
]);
// 1. Décodage Trip Updates (Retards, Suppressions)
if (rtRes.status === 'fulfilled' && rtRes.value && 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 && 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 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 => {
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) {
console.warn(`[sncfApi] Fichier de gare introuvable : ${stationUrl} (statut : ${jsonRes ? jsonRes.status : 'réseau bloqué'})`);
return null;
}
const data = await jsonRes.json();
const gareRef = findGtfsGare(uic);
const targetStationNom = (gareRef && gareRef.nom) || (data && data.nom) || '';
const rawList = mode === 'arrivees' ? data.arrivees : data.departs;
if (!Array.isArray(rawList) || rawList.length === 0) return null;
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')}`;
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000);
const tomorrowDay = tomorrow.getDay();
const tomorrowYYYYMMDD = `${tomorrow.getFullYear()}${String(tomorrow.getMonth() + 1).padStart(2, '0')}${String(tomorrow.getDate()).padStart(2, '0')}`;
// Helper de vérification des règles de circulation calendaires
const circulatesOnDate = (t, targetDay, targetYYYYMMDD, targetDate) => {
const pad = (n) => String(n).padStart(2, '0');
const targetDateHyphen = `${targetDate.getFullYear()}-${pad(targetDate.getMonth() + 1)}-${pad(targetDate.getDate())}`;
// 1. Base GTFS : si t.dates existe (tableau précis de dates YYYYMMDD), c'est la source de vérité absolue
if (Array.isArray(t.dates) && t.dates.length > 0) {
return t.dates.includes(targetYYYYMMDD) || t.dates.includes(targetDateHyphen);
}
// 2. Base Supabase : jours personnalisés
const customDaysRaw = t.jours_personnalises;
if (customDaysRaw) {
let customDays = [];
if (Array.isArray(customDaysRaw)) {
customDays = customDaysRaw;
} else if (typeof customDaysRaw === 'string') {
try { customDays = JSON.parse(customDaysRaw); } catch (_) { customDays = []; }
}
if (customDays.includes(targetDateHyphen)) return true;
}
// 3. Base Supabase : circulation standard par jour de semaine
const daysMap = ['circule_dimanche', 'circule_lundi', 'circule_mardi', 'circule_mercredi', 'circule_jeudi', 'circule_vendredi', 'circule_samedi'];
const colName = daysMap[targetDay];
if (typeof t[colName] !== 'undefined') {
const dd = t.date_debut ? String(t.date_debut).slice(0, 10) : null;
const df = t.date_fin ? String(t.date_fin).slice(0, 10) : null;
if (dd && targetDateHyphen < dd) return false;
if (df && targetDateHyphen > df) return false;
return Boolean(t[colName]);
}
// 4. Jours GTFS (fallback si t.dates n'était pas fourni)
if (Array.isArray(t.days) && t.days.length > 0) {
if (!t.days.includes(targetDay)) return false;
if (t.start && targetYYYYMMDD < String(t.start).replace(/-/g, '')) return false;
if (t.end && targetYYYYMMDD > String(t.end).replace(/-/g, '')) return false;
return true;
}
// 5. Date unique fixée (date_depart, date, etc.)
const singleDate = t.date_depart || t.date || t.service_date;
if (singleDate) {
const sStr = String(singleDate).slice(0, 10);
return sStr === targetDateHyphen || sStr.replace(/-/g, '') === targetYYYYMMDD;
}
return true;
};
// Helper de résolution de quai en temps réel (Estimated Timetable + Situation Exchange + OpenData)
const lookupPlatform = (trainNum, tripId, stationUic, stationName, rawTimeStr = '', destName = '') => {
const cNum = cleanNum(trainNum);
const rawNum = String(trainNum || '').replace(/\D/g, '');
const cName = stationName ? formatStationName(stationName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
const cDest = destName ? formatStationName(destName).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
const uicClean = String(stationUic || '').replace(/\D/g, '');
const uic7 = (uicClean.length === 8 && uicClean.startsWith('87')) ? uicClean.slice(2) : '';
return (
// 1. SIRI-Lite Estimated Timetable (60 prochaines min - Priorité 1)
(cNum && uicClean && siriData.platforms.get(`${cNum}_${uicClean}`)) ||
(rawNum && uicClean && siriData.platforms.get(`${rawNum}_${uicClean}`)) ||
(uicClean && cNum && siriData.platforms.get(`${uicClean}_${cNum}`)) ||
(cNum && uic7 && siriData.platforms.get(`${cNum}_${uic7}`)) ||
(uic7 && cNum && siriData.platforms.get(`${uic7}_${cNum}`)) ||
(cNum && cName && siriData.platforms.get(`${cNum}_${cName}`)) ||
(rawNum && cName && siriData.platforms.get(`${rawNum}_${cName}`)) ||
(tripId && uicClean && siriData.platforms.get(`${tripId}_${uicClean}`)) ||
(tripId && cName && siriData.platforms.get(`${tripId}_${cName}`)) ||
// 2. SIRI-Lite Situation Exchange (Circulations à venir / Changements de quai)
(cNum && uicClean && sxData.platforms.get(`${cNum}_${uicClean}`)) ||
(rawNum && uicClean && sxData.platforms.get(`${rawNum}_${uicClean}`)) ||
(uicClean && cNum && sxData.platforms.get(`${uicClean}_${cNum}`)) ||
(cNum && cName && sxData.platforms.get(`${cNum}_${cName}`)) ||
(rawNum && cName && sxData.platforms.get(`${rawNum}_${cName}`)) ||
// 3. OpenData SNCF direct
(cNum && uicClean && openDataPlatforms.get(`${cNum}_${uicClean}`)) ||
(rawNum && uicClean && openDataPlatforms.get(`${rawNum}_${uicClean}`)) ||
(cNum && cName && openDataPlatforms.get(`${cNum}_${cName}`)) ||
(cNum && cDest && openDataPlatforms.get(`${cNum}_${cDest}`)) ||
''
);
};
// Helper d'enrichissement d'un train
const mapTrain = (t, idx, isTomorrow = false) => {
const rawTime = mode === 'arrivees' ? (t.arr || t.dep) : (t.dep || t.arr);
const timeHM = parseTimeHM(rawTime) || 0;
// Recherche de mise à jour GTFS-RT, SIRI-Lite Timetable ou SIRI-Lite Situation Exchange
const rt = !isTomorrow ? (rtData.updates.get(t.trip_id) || (t.num ? rtData.updates.get(t.num) : null) || (cleanNum(t.num) ? rtData.updates.get(cleanNum(t.num)) : null)) : null;
const siri = !isTomorrow ? (siriData.updates.get(t.num) || (cleanNum(t.num) ? siriData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? siriData.updates.get(t.trip_id) : null)) : null;
const sx = !isTomorrow ? (sxData.updates.get(t.num) || (cleanNum(t.num) ? sxData.updates.get(cleanNum(t.num)) : null) || (t.trip_id ? sxData.updates.get(t.trip_id) : null)) : null;
const isCanceled = (rt && rt.isCanceled) || (siri && siri.isCanceled) || (sx && sx.isCanceled) || false;
const delayMinutes = (rt && rt.delayMinutes > 0) ? rt.delayMinutes : (siri && siri.delayMinutes > 0 ? siri.delayMinutes : 0);
const currentStationName = targetStationNom || (data && data.nom) || (mode === 'arrivees' ? t.dest : t.orig);
const destinationStation = mode === 'arrivees' ? t.orig : t.dest;
const isAutocar = (
(t.type && (t.type.toUpperCase().includes('AUTOCAR') || t.type.toUpperCase().includes('CAR') || t.type.toUpperCase() === 'BUS')) ||
(t.type_train && (t.type_train.toUpperCase().includes('AUTOCAR') || t.type_train.toUpperCase().includes('CAR') || t.type_train.toUpperCase() === 'BUS')) ||
(t.op && (t.op.toUpperCase().includes('AUTOCAR') || t.op.toUpperCase().includes('CAR') || t.op.toUpperCase() === 'BUS')) ||
Boolean(t.is_substitution) ||
isCarNumber(t.num || t.numero_train || t.numero)
);
let realVoie = lookupPlatform(t.num, t.trip_id, uic, currentStationName, rawTime, destinationStation);
if (!realVoie && (t.quai || t.quai_reel || t.platform || t.voie)) {
realVoie = String(t.quai || t.quai_reel || t.platform || t.voie).replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
}
if (!realVoie) {
realVoie = isAutocar ? 'AUTOCAR' : resolveNominalTrack(currentStationName, t.num, uic);
}
let statusLabel = "à l'heure";
let etat = 'a_l_heure';
if (isCanceled) {
statusLabel = 'supprimé';
etat = 'supprime';
} else if (delayMinutes > 0) {
statusLabel = `retardé +${delayMinutes}min.`;
etat = 'retarde';
}
let cause = (sx && sx.cause) || '';
if (!isTomorrow && delayMinutes > 0 && !cause) {
if (rtData.alerts.length > 0) {
cause = rtData.alerts[idx % rtData.alerts.length].text;
} else if (sxData.alerts.length > 0) {
cause = sxData.alerts[idx % sxData.alerts.length].text;
}
}
const formattedStops = (t.stops || []).map((st) => {
const stopUic = st.uic || '';
let stopVoie = lookupPlatform(t.num, t.trip_id, stopUic, st.nom, st.heure, destinationStation);
if (!stopVoie && (st.quai || st.quai_reel || st.voie || st.platform)) {
stopVoie = String(st.quai || st.quai_reel || st.voie || st.platform).replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
}
if (!stopVoie) {
stopVoie = isAutocar ? 'AUTOCAR' : resolveNominalTrack(st.nom, t.num);
}
return {
uic: stopUic,
nom: formatStationName(st.nom),
heure: formatTimeHM(st.heure),
quai: stopVoie,
quai_reel: stopVoie,
voie: stopVoie,
platform: stopVoie
};
});
return {
id: `gtfs-${isTomorrow ? 'demain-' : ''}${t.num || idx}-${rawTime}-${idx}`,
trip_id: t.trip_id,
numero_train: t.num || String(1000 + idx),
type_train: t.type || 'TER',
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'),
_isTomorrow: isTomorrow,
_displayTime: formatTimeHM(rawTime),
_displayPlatform: realVoie,
_depHM: timeHM,
_arrHM: timeHM,
_rawTime: rawTime,
_status: {
label: statusLabel,
detail: '',
delayMinutes: delayMinutes || null,
causeDetails: cause || null
}
};
};
// 1. Filtrer les trains circulant aujourd'hui
let rawToday = rawList.filter(t => circulatesOnDate(t, currentDay, currentYYYYMMDD, now));
if (rawToday.length === 0) {
// Fallback si pas de calendrier GTFS explicite
rawToday = rawList.filter(t => Array.isArray(t.days) && t.days.includes(currentDay));
if (rawToday.length === 0) rawToday = rawList;
}
const enrichedToday = rawToday.map((t, idx) => mapTrain(t, idx, false));
// Filtrer les trains passés : "Une fois que l'heure de départ / Arrivée est dépassée, alors ne plus l'afficher"
const remainingToday = enrichedToday
.filter(t => {
const trainHM = (mode === 'arrivees' ? t._arrHM : t._depHM) || 0;
const delay = t.retard_minutes || 0;
return (trainHM + delay) >= currentHM;
})
.sort((a, b) => {
const aHM = (mode === 'arrivees' ? a._arrHM : a._depHM) || 0;
const bHM = (mode === 'arrivees' ? b._arrHM : b._depHM) || 0;
return aHM - bHM;
});
// 2. Filtrer les trains circulant demain
let rawTomorrow = rawList.filter(t => circulatesOnDate(t, tomorrowDay, tomorrowYYYYMMDD, tomorrow));
if (rawTomorrow.length === 0) {
rawTomorrow = rawList.filter(t => Array.isArray(t.days) && t.days.includes(tomorrowDay));
}
const enrichedTomorrow = rawTomorrow
.map((t, idx) => mapTrain(t, idx, true))
.sort((a, b) => {
const aHM = (mode === 'arrivees' ? a._arrHM : a._depHM) || 0;
const bHM = (mode === 'arrivees' ? b._arrHM : b._depHM) || 0;
return aHM - bHM;
});
// 3. Logique : s'il reste <= 5 horaires aujourd'hui, afficher ceux d'aujourd'hui + ceux du lendemain
let resultList = [];
if (remainingToday.length <= 5) {
resultList = [...remainingToday, ...enrichedTomorrow];
} else {
resultList = remainingToday;
}
if (resultList.length === 0) {
resultList = (enrichedToday.length > 0 ? enrichedToday : rawList.map((t, idx) => mapTrain(t, idx, false))).sort((a, b) => {
const aHM = (mode === 'arrivees' ? a._arrHM : a._depHM) || 0;
const bHM = (mode === 'arrivees' ? b._arrHM : b._depHM) || 0;
return aHM - bHM;
});
}
return resultList;
} 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);
} else {
// Si UIC non trouvé directement, chercher par nom
const match = findGtfsGare(gareName);
if (match && match.uic) {
finalTrains = await fetchGtfsStationTimetable(match.uic, mode);
}
}
if (!finalTrains) {
finalTrains = [];
}
// 3. Filtrage strict par quai si demandé
if (quai) {
const cleanP = (p) => String(p || '').toLowerCase().replace(/^(?:voie|quai|track|platform)\s*/i, '').replace(/\s+/g, '').trim();
const targetP = cleanP(quai);
finalTrains = finalTrains.filter(t => {
const q = cleanP(t.quai || t.voie || t.quai_reel || t._displayPlatform);
return Boolean(q && q === targetP);
});
}
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);
}