Affichage détaillé d'un voyage
ReCHor – étape 9
1. Introduction
Le but de cette étape est de commencer l'interface graphique de ReCHor en écrivant une classe permettant d'afficher un voyage de manière détaillée.
2. Concepts
2.1. Affichage détaillé d'un voyage
Comme nous l'avons vu dans l'introduction au projet, l'interface graphique de ReCHor est composée de trois parties principales, qui sont numérotées sur la figure 1 plus bas :
- les champs de requête permettant d'entrer les noms des arrêts de départ et d'arrivée, et la date/heure de voyage,
- la vue d'ensemble de tous les voyages correspondant à la requête,
- la vue détaillée du voyage sélectionné.
Le but de cette étape est d'écrire le code permettant de créer la troisième de ces parties, celle qui montre les détails du voyage sélectionné.

La vue détaillée d'un voyage présente ses différentes étapes, ordonnées de haut en bas. Pour les étapes à pied, elle montre leur type — changement au sein d'une gare, ou trajet entre deux gares — et leur durée. Pour les étapes en véhicule, elle montre :
- l'heure, la gare et la voie/quai de départ,
- une icône représentant le mode de transport (train, bus, etc.),
- le nom de la ligne et la destination finale de la course,
- s'il y a au moins un arrêt intermédiaire, leur nombre et la durée de l'étape,
- l'heure, la gare et la voie/quai d'arrivée.
Les informations liées aux départs étant généralement plus importantes que celles liées aux arrivées, l'heure et la voie/quai de départ sont affichées en gras.
Pour faciliter l'identification des étapes en véhicule, des cercles noirs sont affichés à gauche des noms des gares de départ et d'arrivée. Ces deux cercles sont reliés par une ligne rouge.
Les arrêts intermédiaires sont masqués par défaut, mais il est possible de les afficher en cliquant sur l'élément présentant leur nombre, ce qui a été fait pour la seconde étape en véhicule dans la figure 1. Lorsqu'ils sont affichés, les arrêts intermédiaires sont présentés avec leur heure d'arrivée, leur heure de départ, et leur nom.
Les éléments de cette vue sont généralement alignés à gauche, sauf les heures de départ et d'arrivée qui sont toujours alignées à droite, et l'icône représentant le mode de transport, qui est centrée horizontalement et verticalement dans la zone qu'elle occupe.
Au-dessous des étapes se trouvent deux boutons, l'un intitulé « Carte » et permettant de visualiser le tracé du voyage sur un site Web externe, l'autre intitulé « Calendrier » et permettant d'exporter dans un fichier le voyage au format iCalendar.
Lorsqu'aucun voyage n'existe, la vue détaillée affiche simplement le texte Aucun voyage en son centre, comme illustré à la figure 2 ci-dessous.

2.2. Affichage du tracé du voyage
Le bouton « Carte » au bas des étapes du voyage permet d'afficher, dans un navigateur Web, une carte montrant son tracé. Le site que nous utiliserons pour cela est l'instance de uMap généreusement mise à disposition par OpenStreetMap Suisse.
Ce site a été choisi car il permet d'afficher facilement un document GeoJSON qui peut être transmis dans la requête attachées à une URL1. Par exemple, pour afficher le document GeoJSON présenté en exemple à l'étape précédente, à savoir :
il suffit de supprimer tous les espaces et retours à la ligne qu'il contient, puis de l'ajouter à la fin d'une URL ayant la forme suivante :
https://umap.osm.ch/fr/map/?data=
On obtient alors l'URL suivante :
qui permet d'accéder à la carte montrant le tracé.
2.3. Sauvegarde de l'événement iCalendar
Le bouton « Calendrier » au bas des étapes du voyage permet de l'exporter dans un fichier, au format iCalendar.
Lorsqu'on clique sur ce bouton, une boîte de dialogue s'affiche afin de choisir le fichier dans lequel l'événement doit être stocké. Par défaut, le nom de ce fichier est composé du préfixe voyage
, d'un caractère souligné (_
), de la date du voyage au format ISO et de l'extension .ics
qui identifie les événements iCalendar. Par exemple, pour un voyage ayant lieu le 15 avril 2025, le nom proposé par défaut est :
voyage_2025-04-15.ics
3. Mise en œuvre Java
Avant de commencer à programmer cette étape, il vous faut télécharger une archive Zip que nous mettons à votre disposition et qui contient deux fichiers :
module-info.java
(dans le dossierReCHor/src
)- à placer dans le dossier
src
de votre projet, pour les raisons décrites à la section suivante, details.css
(dans le dossierReCHor/resources
)- à placer dans le dossier
resources
de votre projet, au même niveau que les images commeAERIAL_LIFT.png
.
Le fichier details.css
est une feuille de style (cascading style sheets ou CSS en anglais), qui décrit l'aspect des éléments de l'interface graphique. Il n'est pas nécessaire de comprendre son contenu, il vous faudra simplement l'attacher à un nœud du graphe de scène, comme décrit plus bas. Les personnes intéressées à en savoir plus à leur sujet pourront néanmoins consulter le document JavaFX CSS Reference Guide.
3.1. Installation de JavaFX
Les deux classes à écrire pour cette étape sont les premières du projet qui ont un lien avec l'interface graphique, ce qui a deux conséquences.
La première est que nous placerons ces classes dans un nouveau sous-paquetage du paquetage principal, nommé gui
.
La seconde est qu'il faut installer JavaFX avant de commencer à rédiger ces classes. Pour cela, suivez les instructions données dans notre guide.
Prenez garde à installer la version 21 de JavaFX, et pas la version 24 car cette dernière n'est pas compatible avec la version 21 de Java que nous utilisons.
Une fois JavaFX installé, placez le fichier module-info.java
fourni dans votre dossier src
. L'existence de ce fichier fait de ReCHor ce que l'on nomme une application modulaire (modular application), c.-à-d. une application utilisant le système de modules de Java. Ce système de modules a été introduit dans la version 9 du langage, est généralement peu utilisé, et ne sera pas examiné en détail dans le cadre de ce cours. Il permet toutefois de simplifier l'utilisation de JavaFX, raison pour laquelle nous avons choisi d'en faire usage dans le projet2.
3.2. Classe VehicleIcons
La classe VehicleIcons
du sous-paquetage gui
, publique et non instanciable, donne accès aux images (icônes) représentant les différents véhicules. Ces images vous ont été fournies dans le squelette de l'étape 1 et se trouvent dans le dossier resources
de votre projet : AERIAL_LIFT.png
, BUS.png
, etc.
La raison d'être de cette classe est que ces images sont très souvent utilisées, mais ne doivent être chargées qu'une seule fois afin de ne pas gaspiller de la mémoire et ralentir le programme. Dès lors, VehicleIcons
charge chaque image lors de sa première utilisation puis la stocke dans un cache, d'où elle est extraite par la suite.
La seule méthode publique de VehicleIcons
, nommée p. ex. iconFor
, prend en argument une valeur de type Vehicle
et retourne l'image JavaFX — de type Image
— correspondante. Il est fondamental que, pour un type de véhicule donné, ce soit toujours la même image — donc la même instance de Image
— qui soit retournée.
3.2.1. Conseils de programmation
Si vous avez correctement indiqué, dans IntelliJ, que le dossier resources
contenant les images était un dossier ressources, alors obtenir une instance de Image
avec l'une des images est extrêmement simple. Par exemple, l'image correspondant au train s'obtient ainsi :
Image train = new Image("TRAIN.png");
Pour représenter le cache des images, une table associative ayant Vehicle
comme type des clefs et Image
comme type des valeurs convient bien. Comme mise en œuvre de cette table, vous pouvez bien entendu utiliser HashMap
ou TreeMap
, mais une autre possibilité consiste à utiliser EnumMap
.
Comme son nom l'indique, EnumMap
n'est utilisable que lorsque le type des clefs est un type énuméré, mais elle est alors extrêmement efficace, encore plus que HashMap
. Son seul problème est qu'elle est légèrement plus difficile à utiliser, car il faut passer à son constructeur l'objet représentant la classe des clefs. Par exemple, si les clefs ont le type Vehicle
, il faut écrire :
Map<Vehicle, Image> myMap = new EnumMap<>(Vehicle.class);
Indépendamment de la mise en œuvre choisie, vous pouvez utiliser la méthode computeIfAbsent
pour écrire la méthode iconFor
en une seule ligne.
3.3. Enregistrement DetailUI
L'enregistrement DetailUI
du sous-paquetage gui
, public, représente la partie de l'interface graphique qui montre les détails d'un voyage.
Pour pouvoir écrire cette classe, vous devez connaître les bases de JavaFX. Dès lors, si vous désirez le faire avant que la matière n'ait été vue au cours, il vous faut lire au minimum les §1 à §3 des notes de cours sur le sujet.
DetailUI
ne possède qu'un seul attribut, de type Node
et nommé p. ex. rootNode
, qui est le nœud JavaFX à la racine de son graphe de scène. La structure de ce graphe est décrite à la §3.3.1 ci-dessous.
DetailUI
possède une seule méthode publique et statique, nommée p. ex. create
, dont le but est de créer le graphe de scène et de retourner une instance de DetailUI
contenant une référence à sa racine. Cette méthode prend en argument une valeur observable de type ObservableValue<Journey>
, contenant le voyage dont les détails doivent être affichés dans l'interface graphique.
Il peut paraître surprenant d'utiliser un enregistrement pour représenter une partie de l'interface graphique, mais cette solution a été choisie car elle permet d'écrire du code relativement simple et concis. L'idée est que chaque partie importante de l'interface graphique sera représentée par un enregistrement dont les attributs contiendront les objets nécessaire à sa connexion avec le reste de l'interface du programme — nœud à la racine du graphe de scène, valeurs observables partagées, etc. — et possédant une méthode create
chargée de créer l'interface.
3.3.1. Graphe de scène
Le graphe de scène des détails d'un voyage est présenté à la figure 3 ci-desous.

Sur cette figure, chaque rectangle blanc représente un nœud JavaFX, et certains d'entre eux sont accompagnés d'annotations colorées qui donnent :
- en vert, les feuilles de style à attacher au nœud en plaçant leur nom dans la liste retournée par
getStylesheets
, - en rouge, l'identité des nœuds, à définir au moyen de la méthode
setId
, - en bleu, les classes de style à associer au nœud en plaçant leur nom dans la liste retournée par
getStyleClass
— il n'y en a pas sur cette figure, mais il y en aura sur d'autres.
Les parties du graphe de scène correspondant aux principaux éléments des détails d'un voyage sont nommées et entourées d'un traitillé. Celle nommée Annotations est destinée à contenir les lignes qui lient les cercles de départ et d'arrivée entre eux, tandis que celle nommée Etapes contient le graphe de scène des étapes, représenté ici par un simple triangle et décrit en détail à la section suivante.
3.3.2. Graphe de scène des étapes
Les étapes du voyage sont représentées dans un panneau organisé en grille (GridPane
) comportant 4 colonnes, chaque étape occupant 1, 3 ou 4 lignes successives de la grille. Ces lignes et colonnes sont numérotées à partir de 0.
Les 4 colonnes de la grille sont généralement dédiées chacune à différentes informations, ainsi :
- colonne 0 : heures de départ et d'arrivée, et icône du véhicule,
- colonne 1 : cercles de départ et d'arrivée,
- colonne 2 : nom de la gare de départ et d'arrivée
- colonne 3 : voie/quai de départ et d'arrivée.
Certaines informations chevauchent toutefois plusieurs colonnes ou lignes, comme décrit ci-après.
Une étape à pied occupe une seule ligne de la grille, constituée d'un simple texte (Text
) qui est celui produit par la méthode formatLeg
de FormatterFr
. Ce texte occupe les colonnes 2 et 3.
Une étape en véhicule occupe 3 ou 4 lignes successives, qui contiennent :
- l'heure de départ (col. 0), le cercle de départ (
Circle
de rayon 3, col. 1), le nom de la gare de départ (col. 2), le nom de la voie/quai de départ (col. 3), - l'icône du véhicule (col. 0) dans une « vue d'image » (
ImageView
), le nom de sa destination produit par la méthodeformatRouteDestination
deFormatterFr
(cols. 2-3), - si l'étape comporte au moins un arrêt intermédiaire : un élément dépliant (
Accordion
) montrant le nombre d'arrêts intermédiaires et la durée de l'étape, ainsi que les arrêts intermédiaires une fois déplié (cols. 2-3), - l'heure d'arrivée (col. 0), le cercle d'arrivée (col. 1), le nom de la gare d'arrivée (col. 2), le nom de la voie/quai d'arrivée (col. 3).
Lorsque l'étape comporte au moins un arrêt intermédiaire, l'icône du véhicule chevauche les lignes 2 et 3, sinon elle n'occupe que la ligne 2. Dans tous les cas, cette icône est affichée à une taille de 31×31 pixels, soit la moitié de sa taille réelle. Cela garantit qu'elle a une apparence agréable, même sur les écrans à haute résolution de type HiDIP/Retina.
Le texte de l'heure de départ et celui du nom de la voie/quai de départ doivent avoir la classe de style departure
associée, c.-à-d. ajoutée à la liste retournée par getStyleClass
.
Les arrêts intermédiaire, s'il y en a, sont aussi représentés dans une grille, qui comporte 3 colonnes : la première contient l'heure d'arrivée à l'arrêt intermédiaire, la seconde l'heure de départ, et la troisième le nom de l'arrêt intermédiaire. La classe de style intermediate-stops
doit être associée à la grille.
3.3.3. Graphe de scène des annotations
Comme cela a été dit plus haut, les lignes reliant les cercles de départ et d'arrivée ne se trouvent pas dans la grille mais dans un panneau séparé, visible sur la figure 3 dans la partie nommée Annotations. Ces lignes ne peuvent en effet pas faire partie de la grille, car elles chevauchent plusieurs cellules, y compris celles qui contiennent déjà les cercles de départ et d'arrivée.
Dès lors, ces lignes ne sont créées qu'une fois que le contenu de la grille a été mis en page, et elles sont placées de manière à ce qu'elles joignent les centres des paires de cercles qu'elles doivent relier. Des indications quant à la manière de faire cela sont données dans la §3.3.4.2 plus bas.
3.3.4. Conseils de programmation
Comme cette classe est la première que vous réalisez en utilisant JavaFX, nous vous conseillons très fortement de l'écrire petit à petit, en procédant ainsi :
- commencez par écrire le code qui construit le graphe de scène principal, sans les annotations ni la représentation des étapes, et vérifiez au moyen du programme de test de la §4 que l'indication Aucun voyage s'affiche correctement,
- écrivez le code permettant de créer le graphe de scène correspondant aux étapes d'un voyage, et passez-lui le contenu actuel de la valeur observable reçue par
create
, que vous obtiendrez au moyen degetValue
; masquez le panneau qui indique Aucun voyage en mettant sa propriétévisible
à faux au moyen desetVisible
, et vérifiez que le voyage est représenté correctement, - écrivez le code permettant de dessiner les lignes reliant les cercles, en suivant les indications données à la §3.3.4.2,
- gérez correctement la mise à jour de la valeur observable passée à
create
.
Afin de faciliter le débogage, réalisez chacune de ces étapes de manière petit à petit, en testant votre code aussi souvent que possible.
- Utilisation de
GridPane
La classe
GridPane
joue un rôle essentiel dans cette étape, et il est donc fondamental de lire sa documentation avant d'entamer la programmation. Ce faisant, vous constaterez qu'un certain nombre de choix très discutables ont malheureusement été faits lors de sa conception, par exemple l'utilisation de méthode statiques commesetHalignment
etsetValignment
pour aligner des nœuds dans la grille. - Création des annotations
Pour créer les lignes reliant les cercles de départ et d'arrivée des étapes en transport — ce que nous avons appelé les annotations — il faut attendre que la grille contenant les étapes ait été mise en page. En effet, ce n'est qu'à ce moment-là que la position des cercles est connue, et donc que celle des lignes les reliant peut l'être également.
Une manière simple de faire cela consiste à stocker la représentation des étapes dans une sous-classe de
GridPane
et de redéfinir sa méthodelayoutChildren
. Dans cette redéfinition, on peut tout d'abord appeler la méthode de la super-classe afin de faire la mise en page, puis utiliser la position des cercles pour déterminer celle des lignes.Dans ce but, on peut stocker dans une liste les paires de cercles à relier entre eux par des lignes. La redéfinition de
layoutChildren
peut alors simplement parcourir cette liste et créer des lignes reliant les centres des cercles. La méthodegetBoundsInParent
est fort utile pour déterminer la position des centres des cercles dans le bon système de coordonnées.Les lignes elles-mêmes sont des instances de
Line
d'une largeur de 2 unités et de couleur rouge (RED
). Ces deux caractéristiques peuvent être changées au moyen des méthodessetStrokeWidth
etsetStroke
. - Visualisation du tracé du voyage sur uMap
Afin de visualiser le tracé du voyage sur uMap, il suffit de construire la bonne URL, puis l'ouvrir dans le navigateur par défaut.
Même s'il existe une classe
URL
dans la bibliothèque Java, il est préférable d'utiliser la classeURI
, plus moderne et mieux conçue, pour représenter une URL. Son constructeur prenant cinq arguments convient bien à nos besoins puisqu'on peut lui passer :https
comme schéma,umap.osm.ch
comme « autorité »,/fr/map
comme chemin d'accès,data=
suivi du document GeoJSON comme requête,null
comme fragment,
afin d'obtenir l'URL à ouvrir. Une fois celle-ci obtenue, il suffit d'appeler la méthode
browse
de l'instance deDesktop
obtenue au moyen degetDesktop
pour l'ouvrir dans le navigateur. - Sauvegarde de l'événement iCalendar
Pour permettre à l'utilisateur de choisir le fichier dans lequel stocker l'événement iCalendar correspondant à un voyage, on utilise une instance de
FileChooser
. Une fois le nom de fichier obtenu, la méthodewriteString
deFiles
permet d'écrire la chaîne représentant l'événement de manière particulièrement simple.
4. Tests
Pour tester cette étape, vous pouvez écrire une application JavaFX minimale qui recherche un voyage de la même manière que le programme de test de l'étape 7, puis affiche ses détails à l'écran :
public final class TestDetailUI extends Application { public static void main(String[] args) { launch(args); } static int stationId(Stations stations, String stationName) { // … comme à l'étape 7 } @Override public void start(Stage primaryStage) throws Exception { TimeTable timeTable = new CachedTimeTable( FileTimeTable.in(Path.of("timetable"))); Stations stations = timeTable.stations(); LocalDate date = LocalDate.of(2025, Month.APRIL, 15); int depStationId = stationId(stations, "Ecublens VD, EPFL"); int arrStationId = stationId(stations, "Gruyères"); Router router = new Router(timeTable); Profile profile = router.profile(date, arrStationId); Journey journey = JourneyExtractor .journeys(profile, depStationId) .get(32); ObservableValue<Journey> journeyO = new SimpleObjectProperty<>(journey); DetailUI detailUI = DetailUI.create(journeyO); Pane root = new BorderPane(detailUI.rootNode()); primaryStage.setScene(new Scene(root)); primaryStage.setMinWidth(400); primaryStage.setMinHeight(600); primaryStage.show(); } }
En exécutant ce programme avec les données horaires de la semaine 16 puis en cliquant sur l'élément pliant contenant les arrêts intermédiaires de la dernière étape, vous devriez voir quelque chose de similaire à l'image ci-dessous.

5. Résumé
Pour cette étape, vous devez :
- écrire les classes
VehicleIcons
etDetailUI
selon les indications plus haut, - tester votre code,
- documenter la totalité des entités publiques que vous avez définies.
Même s'il n'est pas obligatoire de rendre cette étape avant le rendu final (30 mai à 18h00), nous vous conseillons néanmoins fortement de le faire dès que vous l'aurez terminée. Nous conserverons ainsi une copie de sauvegarde de votre travail, que nous pourrons vous fournir en cas de problème.
Notes de bas de page
Une URL, ou adresse Web, est une chaîne de caractères identifiant une ressource (page, image, etc.) sur le Web. Par exemple, l'URL https://www.epfl.ch/ identifie la page principale du site de l'EPFL, https://cs108.epfl.ch/ identifie la page principale de ce cours, etc.
Il faut savoir qu'IntelliJ possède aussi une notion de module qui n'a malheureusement rien à voir avec celle de Java. Par exemple, lorsqu'on examine la structure d'un projet (project structure), les modules qui y figurent sont des modules au sens d'IntelliJ, pas de Java, et ils sont donc présents même en l'absence d'un fichier module-info.java
.