Vue d'ensemble des voyages

ReCHor – étape 10

1. Introduction

Le but de cette avant-dernière étape du projet est d'écrire la classe permettant d'afficher la vue d'ensemble de tous les voyages d'une journée.

2. Concepts

2.1. Vue d'ensemble des voyages

Pour mémoire, l'interface graphique de ReCHor est composée de trois parties principales, qui sont numérotées sur la figure 1 plus bas :

  1. 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,
  2. la vue d'ensemble de tous les voyages correspondant à la requête,
  3. la vue détaillée du voyage sélectionné.

Le but de cette étape est d'écrire le code permettant de créer la deuxième de ces parties, celle qui montre la vue d'ensemble des voyages.

rechor-gui-num;64.png
Figure 1 : Parties principales de l'interface graphique de ReCHor

La vue d'ensemble présente tous les voyages optimaux allant de l'arrêt de départ à l'arrêt d'arrivée pour le jour de voyage choisi. Pour chacun d'entre eux, elle montre :

  • une icône du type de véhicule, le nom de la ligne et la destination finale de la course pour la première étape en véhicule du voyage,
  • l'heure de départ du voyage — qui n'est pas celle de la première étape en véhicule si le voyage commence par une étape à pied,
  • une ligne représentant le voyage, sur laquelle sont placés des disques noirs figurant les arrêts de départ et d'arrivée, et des disques blancs figurant les changements entre deux étapes en véhicule ; ces disques sont situés sur la ligne à la position relative qu'ils occupent dans le voyage,
  • l'heure d'arrivée du voyage,
  • la durée du voyage.

Les voyages sont organisés verticalement dans une liste, triés par heure de départ puis heure d'arrivée croissante.

3. Mise en œuvre Java

Avant de commencer à programmer cette étape, vous devez télécharger une archive Zip qui contient une feuille de style nommée summary.css. Comme d'habitude, vous devez l'ajouter aux ressources de votre projet, et l'attacher au graphe de scène de la manière décrite plus bas.

3.1. Enregistrement SummaryUI

L'enregistrement SummaryUI du sous-paquetage gui, public, représente la partie de l'interface graphique qui montre la vue d'ensemble des voyages. Il possède deux attributs, qui sont :

  • le nœud JavaFX à la racine de son graphe de scène, nommé p. ex. rootNode et de type Node,
  • une valeur observable contenant le voyage sélectionné dans la vue d'ensemble, nommée p. ex. selectedJourneyO — le suffixe O indiquant qu'il s'agit d'une valeur observable — et de type ObservableValue<Journey>.

SummaryUI 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 SummaryUI contenant sa racine ainsi que la valeur observable contenant le voyage sélectionné. Cette méthode prend en arguments :

  • une valeur observable contenant la liste des voyages à afficher, de type ObservableValue<List<Journey>>,
  • une valeur observable contenant l'heure de voyage désirée, de type ObservableValue<LocalTime>.

Lorsque l'heure de voyage désirée change, le premier voyage partant à cette heure-là, ou plus tard, est sélectionné dans la liste. S'il n'y en a aucun, alors le dernier voyage de la liste est sélectionné.

3.1.1. Graphe de scène

Le graphe de scène de la vue d'ensemble des voyages est extrêmement simple puisqu'il est constitué d'une unique instance de ListView qui affiche la liste des voyages. La feuille de style summary.css lui est attaché.

Chaque voyage est représenté par une cellule (cell) de la liste — c.-à-d. l'un de ses éléments — et le graphe de scène de l'une d'entre elles est présenté à la figure 2 ci-desous. Pour mémoire, les étiquettes bleues attachées à certains nœuds donnent les classes de style à leur associer, en modifiant la liste retournée par getStyleClass.

summary-cell-sg;32.png
Figure 2 : Graphe de scène d'une cellule présentant un voyage

Cette figure peut faire penser que deux classes de style (dep-arr et transfer) doivent être associées aux cercles représentant le départ, l'arrivée et les changements du voyage, mais ce n'est bien entendu pas le cas : la classe dep-arr n'est associée qu'aux deux cercles représentant le départ et l'arrivée, tandis que la classe tranfer ne l'est qu'aux éventuels cercles représentant les étapes à pied situées entre deux étapes en véhicule.

3.1.2. Conseils de programmation

  1. Organisation

    La classe SummaryUI n'étant pas totalement triviale à mettre en œuvre, nous vous suggérons une fois encore fortement de procéder en plusieurs phases, qui pourraient consister en :

    1. écrire une version qui utilise une simple chaîne de caractère pour représenter un voyage, ce qui permet d'utiliser une ListView sans devoir changer son type de cellule ; la tester au moyen du programme donné à la §4 plus bas,
    2. en suivant les indications données plus bas, écrire une sous-classe de ListCell<Journey> pour représenter les cellules de la liste des voyages, mais commencer par une version très simple qui n'affiche, par exemple, que l'heure de départ du voyage ; vérifier qu'elle fonctionne avant de continuer,
    3. augmenter progressivement le graphe de scène des cellules — en commençant par les éléments textuels, plus simples -— pour en arriver à celui présenté à la figure 2 plus haut.

    Quelle que soit l'organisation que vous choisissez, commencez par lire la documentation de la classe ListView pour comprendre comment elle fonctionne. De plus, avant d'écrire la sous-classe de ListCell, lisez sa documentation à elle, ainsi que celle de la classe Cell dont elle hérite (indirectement).

  2. Cellule de voyage

    Afin que les voyages aient, dans la liste, l'apparence désirée, il faut définir une classe (privée) héritant de ListCell<Journey>, représentant une « cellule » affichant un voyage. Une fois cette classe définie, il faut encore faire en sorte qu'elle soit utilisée par l'instance de ListView, ce qui se fait en changeant sa « fabrique de cellules » au moyen de la méthode setCellFactory.

    Le constructeur de cette classe doit créer le graphe de scène correspondant à la cellule et présenté à la figure 2. Aucune donnée concrète — heure de départ et d'arrivée, etc. — ne peut toutefois être placée dans ce graphe à ce moment-là, car le voyage associé à la cellule n'est pas encore connu. Ce voyage va d'ailleurs changer au cours de la vie de la cellule, car JavaFX les réutilise pour des raisons d'efficacité.

    Dès lors, en plus d'un constructeur, cette classe doit être équipée d'une redéfinition de la méthode updateItem — dont il faut impérativement lire la documentation pour comprendre comment la redéfinir correctement. Son rôle est de remplir les différents éléments du graphe de scène construit par le constructeur avec les données du voyage à afficher.

    La création du graphe de scène ne pose pas de problèmes particuliers, mais il faut savoir que les icônes représentant le type de véhicule de la première étape doivent y être affichés à une taille de 20×20 unités, et que la ligne montrant les changements n'est construite que dans la méthode updateItem, comme décrit ci-après.

  3. Ligne des changements

    La ligne des changements — qui présente le voyage de manière graphique, avec des disques figurant les arrêts de départ et d'arrivée ainsi que les éventuels changements — n'est pas triviale à définir, pour deux raisons. D'une part, le nombre de disques qu'elle contient dépend du voyage affiché, et d'autre part, elle doit occuper toute la largeur du panneau (Pane) qui la contient, et cette largeur peut changer.

    Dès lors, les disques ne sont pas ajoutés au graphe de scène dans le constructeur, mais bien dans la méthode updateItem.

    De plus, afin que la ligne occupe toute la largeur disponible et que les disques soient correctement positionnés, on place ces nœuds dans une sous-classe (anonyme) de Pane — indiquée par l'astérisque dans la figure 2. La méthode layoutChildren de cette classe est redéfinie afin de dimensionner et positionner correctement la ligne et les disques dans le panneau. La dimension préférée de ce panneau est de plus définie à 0×0 unités — en appelant sa méthode setPrefSize — afin que JavaFX le redimensionne sans tenir compte de son contenu.

    Le placement de la ligne et des disques doit être fait de manière à ce qu'ils soient centrés verticalement dans le panneau qui les contient, et que la ligne occupe (presque) toute sa largeur, en laissant une marge de 5 unités de chaque côté. Les disques représentant les changements ont un rayon de 3 unités.

    Comme cela a été mentionné dans l'introduction, les disques blancs représentant les changements sont placés sur la ligne à la position relative qu'ils occupent dans la durée totale du voyage. Pour être précis, ces disques sont centrés à la position qui correspond à l'heure de départ de l'étape à pied qu'ils représentent.

    Le positionnement de ces disques pose un petit problème puisque les informations permettant de calculer leur position sont connues au moment où setItem est appelée, mais leur positionnement doit être effectué par layoutChildren. Une manière simple de transmettre les informations nécessaires de la première à la seconde méthode consiste à les attacher aux instances de Circle elles-mêmes, au moyen des méthodes setUserData et getUserData.

  4. Voyage sélectionné

    L'un des attributs de SummaryUI est une valeur observable contenant le voyage sélectionné. Il s'agit simplement de l'élément sélectionné de la liste, qu'on obtient aisément de son « modèle de sélection », retourné par la méthode getSelectionModel.

4. Tests

Pour tester cette étape, vous pouvez dupliquer et adapter la classe TestDetailUI de l'étape précédente afin d'obtenir une application JavaFX minimale affichant tous les voyages produits par votre routeur. Elle pourrait ressembler à ceci :

public final class TestSummaryUI extends Application {
  // … comme `TestDetailUI`

  @Override
  public void start(Stage primaryStage) throws Exception {
    // … comme `TestDetailUI`

    List<Journey> journeys = JourneyExtractor
      .journeys(profile, depStationId);

    ObservableValue<List<Journey>> journeysO =
      new SimpleObjectProperty<>(journeys);
    ObservableValue<LocalTime> depTimeO =
      new SimpleObjectProperty<>(LocalTime.of(16, 00));
    SummaryUI summaryUI = SummaryUI.create(journeysO, depTimeO);
    Pane root = new BorderPane(summaryUI.rootNode());

    // … comme `TestDetailUI`
  }
}

En exécutant cette application avec les données horaires de la semaine 18 et en recherchant les voyages entre Ecublens VD, EPFL et Gruyères le 29 avril 2025, vous devriez voir une fenêtre similaire à celle de la figure ci-dessous s'ouvrir. Le voyage débutant à 16h13 devrait être sélectionné, car il est le premier à partir à 16h00 ou plus tard.

summary-ui;64.png
Figure 3 : Vue d'ensemble des voyages EPFL → Gruyères

5. Résumé

Pour cette étape, vous devez :

  • écrire la classe SummaryUI 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.