Etape 6 – Lecture des horaires

1 Introduction

Le but de cette étape est d'écrire la classe permettant de lire les fichiers contenant les horaires des tl et de les charger en mémoire. Le résultat de cette lecture est bien entendu représenté par des instances des classes écrites lors des étapes précédentes.

1.1 Fichiers d'horaire

Pour représenter les horaires des entreprises de transports en commun, un format fréquemment utilisé se nomme GTFS (pour General Transit Feed Specification). Il a été développé par Google dans le cadre de son projet Google Transit. Les horaires nous ont été fournis par les tl dans ce format, à partir duquel nous avons produit une version simplifiée pour le projet, ne contenant que les informations nécessaires. C'est cette version simplifiée du format GTFS qui est décrite ci-dessous.

Fichiers CSV

Dans le format GTFS, les horaires sont spécifiés dans plusieurs fichiers textuels qui sont tous au (pseudo-)format CSV. Il convient donc de décrire rapidement ce format, très commun en informatique.

Le terme CSV (pour comma-separated values) ne désigne pas véritablement un unique format de fichier standard mais plutôt une famille de formats basée les idées communes suivantes :

  1. Un fichier CSV décrit une (et une seule) table rectangulaire formée d'un certain nombre de lignes et de colonnes.
  2. Les fichiers CSV sont textuels (et non binaires comme le sont, par exemple, les fichiers d'images JPEG, les fichiers de musique MP3 ou autres).
  3. Les fichiers CSV sont composés d'une séquence de lignes qui ont toutes la même structure, et en particulier le même nombre de champs.
  4. Les colonnes d'une ligne sont séparées les unes des autres par un unique caractère de séparation qui est souvent la virgule (d'où le nom) ou le point-virgule, le caractère de tabulation, etc.
  5. Parfois, la première ligne contient les en-têtes des différentes colonnes.

Etant donné qu'ils stockent des tables bidimensionnelles, les fichiers CSV sont très souvent utilisés comme format d'échange simple pour les tableurs.

Afin d'illustrer le format CSV au moyen d'un exemple, prenons la table ci-dessous, qui contient une liste des étudiants suivant un cours et leur note à deux examens :

Nom Prénom SCIPER Examen 1 Examen 2
Martin Paulette 912345 6.0 5.5
Bertholet Kévin 922222 4.5 2.0
Della Casa Maria 999888 5.0 4.5
Porchet Jean 965412 2.5 5.5

Cette table pourrait être représenté par un fichier CSV au contenu suivant :

Nom,Prénom,SCIPER,Examen 1,Examen 2
Martin,Paulette,912345,6.0,5.5
Bertholet,Kévin,922222,4.5,2.0
Della Casa,Maria,999888,5.0,4.5
Porchet,Jean,965412,2.5,5.5

Fichiers d'horaire

La description des horaires est donnée dans un total de quatre fichiers :

calendar.csv
contient la description des services, leur plage de validité et leurs jours de circulation,
calendar_dates.csv
contient la description des exceptions aux jours de circulation des services, c-à-d les jours où les services sont exceptionnellement actifs ou inactifs,
stops.csv
contient la description des arrêts, c-à-d leur nom et leur position géographique,
stop_times.csv
contient la description de ce que nous avons appelé les trajets.

Il s'agit de fichiers CSV utilisant le point-virgule (;) comme séparateur de champs et l'encodage des caractères UTF-8. Contrairement à l'exemple ci-dessus, ces fichiers sont dépourvus d'une ligne d'en-tête, c-à-d que la première ligne représente déjà des données.

Services

Les services sont décrits par les deux fichiers calendar.csv et calendar_dates.csv.

Le fichier calendar.csv, qui décrit les services sans leurs exceptions, est formé de lignes ayant chacune les 10 champs suivants, dans l'ordre :

  • le nom du service,
  • les jours de circulation, du lundi au dimanche, occupant chacun un champ dont la valeur est 1 si le service est actif ce jour-là et 0 sinon,
  • la date de début du service composée de 8 chiffres sans séparateurs, les quatre premiers étant l'année, les deux suivants le mois, les deux derniers le jour du mois,
  • la date de fin du service, au même format.

Par exemple, un fichier calendar.csv contenant les deux lignes suivantes :

Semaine;1;1;1;1;1;0;0;20130101;20131231
Samedi;0;0;0;0;0;1;0;20130101;20131231

décrit les deux services suivants :

Nom Lu Ma Me Je Ve Sa Di Début Fin
Semaine     2013-01-01 2013-12-31
Samedi             2013-01-01 2013-12-31

Le premier de ces deux services se nomme donc Semaine et circule du lundi au vendredi du 1er janvier 2013 au 31 décembre. Le second se nomme Samedi et circule le samedi dans la même plage.

Les exceptions aux jours de circulation sont spécifiés par le fichier calendar_dates.csv dont les lignes ont chacune les 3 champs suivants :

  • le nom du service,
  • la date de l'exception,
  • le type de l'exception, qui vaut 1 si le service est exceptionnellement actif ce jour-là, et 2 si le service est exceptionnellement inactif ce jour-là.

Par exemple, un fichier calendar_dates.csv contenant les deux lignes suivantes :

Samedi;20130916;1
Semaine;20130916;2

spécifie que le service Samedi est exceptionnellement actif le 16 septembre 2013, tandis que le service Semaine est exceptionnellement inactif ce jour-là.

Arrêts

Le fichier stops.csv contient l'ensemble des arrêts du réseau. Chaque arrêt est spécifié par son nom, sa latitude et sa longitude (dans le système WGS 84). Par exemple, un fichier stops.cvs contenant les deux lignes suivantes :

Val-Vert;46.5219111511;6.66704849314
Rosiaz;46.5217497237;6.66227652505

décrit les deux arrêts suivants :

Nom Latitude Longitude
Val-Vert 46.5219111511 6.66704849314
Rosiaz 46.5217497237 6.66227652505

Trajets

Le fichier stop_times.csv contient l'ensemble des trajets du réseau. Chaque ligne décrit un trajet en donnant (dans l'ordre) le nom du service auquel ce trajet est rattaché, le nom de l'arrêt de départ, l'heure de départ (en secondes après minuit), le nom de l'arrêt d'arrivée et l'heure d'arrivée.

Par exemple, un fichier stop_times.csv contenant les deux lignes suivantes :

Semaine;Val-Vert;34320;Rosiaz;34380
Semaine;Rosiaz;34380;Coudrette;34380

décrit les deux trajet suivants :

Service Départ Heure dép. Arrivée Heure arr.
Semaine Val-Vert 09:32:00 Rosiaz 09:33:00
Semaine Rosiaz 09:33:00 Coudrette 09:33:00

Données de test et réelles

Nous mettons à votre disposition deux jeux de données horaires : un petit jeu de test et le jeu complet des données fournies par les tl.

Le jeu de test est disponible publiquement dans l'archive Zip test-data-tl-2013.zip et vous permet d'effectuer des tests pour cette étape.

Le jeu complet n'est quant à lui pas disponible publiquement, pour des raisons de confidentialité des données. Pour l'obtenir, les membres de chaque groupe doivent signer un accord attestant qu'ils ne redistribueront pas ces données, et qu'ils ne les utiliseront pas à d'autres fins que ce projet. Sur chacun de ces accords figure un lien unique, associé au groupe, que celui-ci utilisera pour accéder à sa version personnelle des données. L'accord à signer s'obtient auprès des assistants durant les séances d'exercices.

Une fois l'un des jeux de données téléchargé, il convient de l'importer dans Eclipse de la manière suivante :

  1. En prenant bien garde à ce que votre projet soit sélectionné dans l'explorateur de paquetages (Package Explorer), importez le contenu de l'archive Zip des horaires, comme vous avez l'habitude de le faire pour importer des tests.
  2. Dans votre explorateur de paquetage, vous devriez maintenant avoir un répertoire nommé data au même niveau que le répertoire src contenant les fichiers Java de votre projet. Faites un clic droit sur ce répertoire et, dans le menu contextuel qui s'ouvre alors, choisissez l'action Use as Source Folder du sous-menu Build Path. L'icône associée à ce répertoire data change alors pour être identique à celle du répertoire src, indiquant le succès de l'opération.

Une fois cette importation faite, il est possible d'accéder aux fichiers de données depuis Java, au moyen de la notion de ressource décrite plus bas.

2 Mise en œuvre Java

La mise en œuvre Java de la lecture des horaires se fait dans une seule classe. Avant de passer à sa description, il convient toutefois de donner quelques explications rapides concernant l'accès aux fichiers en Java.

2.1 Lecture de fichiers

Pour lire des fichiers, la bibliothèque Java offre le concept de flot, dont il existe deux variantes :

  1. les flots d'entrée (input streams), qui permettent la lecture de fichiers binaires et qui sont modélisés par des sous-classes de la classe InputStream du paquetage java.io, et
  2. les lecteurs (readers), qui permettent la lecture de fichiers textuels et qui sont modélisés par des sous-classes de la classe Reader du paquetage java.io.

Les flots d'entrée et les lecteurs fonctionnent selon un principe similaire. Tous deux modélisent des flots (c-à-d des séquences) de valeurs, ces valeurs étant des octets pour les flots d'entrée et des caractères pour les lecteurs. L'opération principale que l'on peut effectuer sur un flot est d'obtenir sa prochaine valeur, au moyen d'une méthode qui s'appelle généralement read. Cette méthode de lecture consomme et retourne la prochaine valeur du flot, dans le sens où l'appel suivant retournera la valeur d'après, et ainsi de suite jusqu'à épuisement des valeurs.

En résumé, les flots d'entrée et les lecteurs sont très similaires aux itérateurs des collections, leur méthode read jouant le même rôle que la méthode next de ces derniers. Les flots diffèrent des itérateurs en deux points importants :

  1. ils ne disposent pas d'équivalent à la méthode hasNext des itérateurs, mais leur méthode read retourne une valeur spéciale (p.ex. -1 ou null) pour indiquer que la fin du flot a été atteinte,
  2. ils disposent d'une méthode close permettant de les fermer, ce qui a pour effet de libérer, au niveau du système d'exploitation, les ressources qui leurs sont allouées.

Il est très important de ne pas oublier d'appeler la méthode close sur les flots d'entrée ou sur les lecteurs dès la fin de leur utilisation.

Les fichiers à lire pour cette étape étant textuels, seules des sous-classes de Reader doivent être utilisées. Deux d'entre-elles sont particulièrement utiles ici :

  • InputStreamReader, qui transforme un flot d'entrée en un lecteur, en décodant les octets pour les transformer en caractères, selon un schéma d'encodage donné (p.ex. UTF-8).
  • BufferedReader, qui augmente un lecteur en lui ajoutant la méthode readLine qui lit les données du flot ligne par ligne plutôt que caractère par caractère, en retournant null lorsque la fin du flot a été atteinte. En d'autres termes, BufferedReader transforme un flot de caractères en un flot de lignes.

L'extrait de code ci-dessous, dont vous pouvez vous inspirer pour cette étape, illustre l'utilisation de ces différentes classes en affichant à l'écran toutes les lignes d'un flot d'entrée inStream. Le standard UTF-8 est utilisé pour décoder les caractères :

InputStream inStream = …;
BufferedReader reader =
    new BufferedReader(
        new InputStreamReader(inStream, StandardCharsets.UTF_8));
String line;
int i = 1;
while ((line = reader.readLine()) != null) {
    System.out.println("ligne " + (i++) + ": " + line);
}
reader.close();

La classe StandardCharsets provient du paquetage java.nio.charset et a été introduite avec la version 1.7 de la bibliothèque, qu'il vous faut donc utiliser pour pouvoir compiler ce code.

2.2 Ressources

Avant de pouvoir effectivement lire les données d'horaire depuis un fichier, il faut encore savoir comment localiser ces fichiers afin de pouvoir obtenir un flot d'entrée avec leur contenu.

Une possibilité serait de placer les fichiers quelque part sur disque puis d'utiliser la classe FileInputStream pour créer un flot d'entrée avec le contenu des différents fichiers.

Toutefois, il existe un moyen encore plus simple pour y avoir accès. En effet, la bibliothèque Java offre une notion de ressources, qui sont des fichiers que l'on associe à un projet en les stockant dans le même répertoire que le code compilé (c-à-d les fichiers .class). L'importation des données dans Eclipse décrite ci-dessus a justement pour but de faire en sorte que les fichiers d'horaire soient des ressources.

Si vous avez bien suivi cette procédure d'importation, vous devriez par exemple pouvoir obtenir un flot d'entrée avec le contenu du fichier stops.csv au moyen du morceau de code suivant :

InputStream stopsStream =
    getClass().getResourceAsStream("/time-table/stops.csv");

En combinant ce code avec celui donné plus haut, il est aisé de lire les quatre fichiers contenant les horaires, ligne après ligne.

2.3 Classe TimeTableReader

La classe TimeTableReader du paquetage ch.epfl.isochrone.timetable, publique et finale, contient les méthodes de lecture des horaires.

Interface publique

La classe TimeTableReader est équipé d'un unique constructeur publique :

  • TimeTableReader(String baseResourceName), qui construit un lecteur d'horaire ayant la chaîne donnée comme préfixe des ressources. Ce préfixe est placé avant le nom des différents fichiers (p.ex. stops.csv) pour obtenir le nom complet de la ressource. Par exemple, pour lire les fichiers importés précédemment dans Eclipse, il faudra passer /time-table/ en argument à ce constructeur, et ce préfixe sera combiné avec le nom des fichiers individuels comme stops.csv pour obtenir un nom complet de ressource, ici /time-table/stops.csv.

De plus, la classe TimeTableReader contient les deux méthodes publiques suivantes :

  • TimeTable readTimeTable(), qui lit et retourne l'horaire (comme dit dans l'énoncé de l'étape 3, cet horaire ne contient pas les trajets, qui sont décrits par le graphe des horaires, construit par la méthode suivante). Peut lever l'exception IOException en cas d'erreur d'entrée-sortie ou d'autres exceptions en cas d'erreur de format de données.
  • Graph readGraphForServices(Set<Stop> stops, Set<Service> services, int walkingTime, double walkingSpeed), qui lit et retourne le graphe des horaires pour les arrêts donnés, en ne considérant que les trajets dont le service fait partie de l'ensemble donné. Ce graphe inclut également la totalité des trajets à pied entre arrêts qui sont faisables en un temps inférieur ou égal à celui donné (en secondes), à la vitesse de marche donnée (en mètres par secondes). Peut lever l'exception IOException en cas d'erreur d'entrée-sortie ou d'autres exceptions en cas d'erreur de format de données.

Conseils de programmation

Même si la classe TimeTableReader ne contient que deux méthodes publiques, il est néanmoins fortement conseillé d'écrire plusieurs méthodes privées afin de faciliter la compréhension du code. Par exemple, avoir une méthode privée pour chaque fichiers à lire (stops.csv, calendar.csv, etc.) semble judicieux.

Il va de soi que la constructions des différents éléments de l'horaire (TimeTable, Service, Graph, etc.) doit se faire au moyen de leur bâtisseur respectif.

Les méthodes suivantes sont très utiles lors de la lecture des fichiers horaires :

  • split de la classe String, qui découpe une chaîne en fonction d'un caractère de séparation, ce qui permet de découper les lignes en champs,
  • substring de la classe String, qui retourne une sous-chaîne de la chaîne à laquelle on l'applique, ce qui permet p.ex. d'extraire les différents éléments d'une date,
  • parseInt de la classe Integer, qui retourne l'entier correspondant à une chaîne représentant un entier, ce qui permet p.ex. de convertir les chaînes de nombre de secondes après minuit en entiers,
  • parseDouble de la classe Double, qui retourne la valeur réelle correspondant à une chaîne représentant un nombre réel, ce qui permet p.ex. de convertir les chaînes de longitude et latitude en nombres réels.

Beaucoup d'erreurs peuvent potentiellement survenir à la lecture des fichiers, aussi bien des erreurs d'entrée-sortie que des erreurs de format. Toutefois, étant donné que les fichiers ne sont pas ici fournis par l'utilisateur du programme, mais bien intégrés à lui sous la forme de ressources, il est raisonnable de supposer que ces erreurs ne se produiront pas et laisser les exceptions se propager aux appelants des méthodes de TimeTableReader.

2.4 Tests

Comme d'habitude, nous vous fournissons dans l'archive Zip tests-e06.zip des « tests » qui ne font que vérifier que la visibilité et les noms des entités que vous avez définies correspondent à ce qui est attendu.

3 Résumé

Pour cette étape, vous devez :

  1. Ecrire la classe TimeTableReader en fonction des spécifications ci-dessus.
  2. Compléter les classes de test que nous vous fournissons.
  3. Documenter la totalité des entités publiques que vous avez définies.