Tous les articles

·4 min de lecture

Situer un train sur la carte sans aucune donnée de position

La position des trains SNCF en service n'est publiée nulle part. Ramedex l'estime donc en croisant trois sources ouvertes : horaires GTFS, retards SIRI et tracé des voies. Même calcul côté serveur et navigateur, et des limites assumées.

Dans Ramedex, on capture les trains réels qui passent à moins de 12 km de soi. Pour capturer des trains, il faut d'abord savoir où ils sont. Et la réponse est décevante : la position des trains SNCF en service n'est publiée nulle part. Pas d'API, pas de jeu de données, pas de flux temps réel qui donnerait un point GPS. Il a fallu l'estimer, et assez bien, puisque tout le jeu repose dessus.

Ce qui existe, en ouvert, ce sont trois briques.

  • Les horaires théoriques de tous les trains, au format GTFS. Chaque parcours, chaque gare desservie, chaque heure d'arrivée et de départ.
  • Les retards en temps réel, au format SIRI Lite, rafraîchis toutes les 2 minutes.
  • Le tracé géométrique des voies, publié par SNCF Réseau pour les lignes ouvertes et complété par OpenStreetMap pour le reste.

Aucune des trois ne donne une position. Prises ensemble, elles permettent d'en déduire une.

Une fraction de temps, une fraction de voie

Entre deux gares consécutives, le calcul tient en quelques lignes. Je pars de l'heure de départ en gare A et de l'heure d'arrivée en gare B, toutes deux décalées du dernier retard connu pour ce train. L'heure actuelle me donne la fraction du temps écoulé depuis le départ. En version simplifiée :

// Progression d'un train sur un tronçon, entre 0 et 1.
function progression(depart, arrivee, maintenant) {
  if (maintenant <= depart) return 0;
  if (maintenant >= arrivee) return 1;
  return (maintenant - depart) / (arrivee - depart);
}

Cette fraction n'est pas appliquée en ligne droite entre les deux gares, mais à la longueur du tracé réel du tronçon. Le tracé est une polyligne, chaque segment a une longueur, je les cumule et je m'arrête au point qui correspond à la fraction voulue.

La ligne droite entre deux gares est l'idée qui vient en premier, et pour le jeu elle serait presque suffisante. Le rayon de capture est de 12 km, une voie s'écarte rarement autant de sa corde. Mais visuellement, ça ne passe pas. Une ligne ferrée suit une vallée et contourne les reliefs. Des trains qui coupent à travers les champs, sur une carte où les rails sont dessinés, ça se voit tout de suite. Quand le tracé manque, la ligne droite reste le repli.

L'interpolation suppose une vitesse constante, ce qui est faux. Un train accélère au départ, ralentit à l'approche, marque l'arrêt. Sur un tronçon qui se parcourt en plusieurs minutes, ces écarts se compensent en partie, et un jeu de collection n'a pas besoin de mieux.

À quai, sur la voie annoncée

Entre son heure d'arrivée et son heure de départ dans une gare, le train ne roule pas. L'interpolation le ferait glisser pendant l'arrêt, donc ce créneau est un cas à part. Le train est à quai. Si la voie d'arrivée est annoncée, je le place dessus, avec une petite mise au propre des libellés. 1B désigne un secteur de la voie 1 : je cherche d'abord ce secteur, et s'il n'est pas connu, je retombe sur la voie 1. Si la voie est inconnue, le train est placé au point générique de la gare, le bâtiment voyageurs. C'est grossier pour une grande gare, où les quais s'étalent largement, mais l'arrêt est bref.

Le même calcul des deux côtés

Ce calcul tourne à deux endroits. Sur le serveur, tous les trains en circulation sont recalculés toutes les 30 secondes, une seule fois pour tous les joueurs. Dans le navigateur, la même formule fait avancer les trains entre deux rafraîchissements, pour que l'animation soit continue. Et quand un joueur capture un train, le serveur recalcule la position de ce train au moment de la capture pour valider.

Ça impose une contrainte : la formule doit être exactement la même des deux côtés. Si le navigateur animait les trains avec autre chose qu'une interpolation linéaire, un easing par exemple, la position affichée et la position validée divergeraient. Le joueur viserait un endroit, le serveur en jugerait un autre. Même interpolation, mêmes décalages de retard, même traitement du cas à quai. Le calcul existe en deux langages, du PHP côté serveur, du JavaScript côté navigateur.

Reste à faire transiter les tracés jusqu'au téléphone. Un tronçon, c'est une suite de points, et à l'échelle du réseau entier, ça représente bien trop de données pour tout envoyer d'un coup. Le navigateur ne reçoit que le tracé du tronçon en cours et des tronçons suivants, de quoi tenir jusqu'au prochain rafraîchissement. Chaque tracé est envoyé une seule fois, encodé en polyline, et les trains y font référence par une clé. Si deux trains se suivent sur la même ligne, le tracé ne part qu'une fois.

Les limites sont connues et assumées. Je ne garde qu'un retard par train, mis à jour à chaque relevé, toutes les 2 minutes, et je l'applique à tous ses horaires. Un retard pris en cours de route n'apparaît donc qu'au relevé suivant. Ensuite, quand le tracé manque, ligne droite, et le train ne suit plus exactement la voie. Enfin, on affiche un train en retard jusqu'à 3 heures après son arrivée théorique, puis on abandonne : au-delà, on ne sait plus assez de choses sur lui pour continuer à l'estimer.

Au fond, la géométrie n'est pas le point faible du système. Même avec quelques centaines de mètres d'erreur, une position reste invisible à l'échelle d'un rayon de capture de 12 km. Le point faible est le temps. Un retard pris en cours de tronçon n'existe pas encore pour le système. Le train affiché est en avance sur le vrai, jusqu'au relevé suivant, où tout se recale.