Interface de recherche de voyages
ReCHor – étape 11
1. Introduction
Le but de cette dernière étape est de terminer le projet en écrivant le code gérant l'interface qui permet de paramétrer la recherchee de voyages, ainsi que le programme principal.
2. Concepts
2.1. Interface de recherche de voyages
L'interface de recherche de voyages est celle portant le numéro 1 dans l'image ci-dessous.

Cette partie de l'interface graphique est celle qui permet à un utilisateur du programme d'entrer les paramètres du voyage qu'il désire effectuer, à savoir :
- le nom de l'arrêt de départ,
- le nom de l'arrêt d'arrivée,
- la date du voyage,
- l'heure de départ du voyage.
Les noms d'arrêts sont vides par défaut, tandis que la date et l'heure du voyage sont la date et l'heure actuelles au moment du démarrage du programme.
Lorsqu'aucun arrêt de départ ou d'arrivée n'est entré, un texte d'invite apparaît en grisé dans le champ textuel correspondant. Ce texte est Nom de l'arrêt de départ pour le champ de l'arrêt de départ, et Nom de l'arrêt d'arrivée pour celui de l'arrêt d'arrivée. Finalement, le bouton situé entre l'arrêt de départ et d'arrivée, qui montre une double flèche, permet d'échanger les deux arrêts.
Lorsqu'un des deux champs textuels permettant de choisir un arrêt est actif, le texte qu'il contient est interprété comme une requête de recherche d'arrêt, et les 30 meilleurs résultats de la recherche sont présentés, du plus au moins pertinent, dans une fenêtre qui apparaît sous le champ textuel :

Cette fenêtre montre environ 10 résultats, et le premier d'entre eux est initialement sélectionné. Il est possible d'en sélectionner un autre soit au moyen de la souris, soit en utilisant les touches du curseur — ↑ pour sélectionner l'élément précédent, ↓ pour sélectionner le suivant.
Lorsque le curseur quitte le champ textuel, par exemple lorsque l'utilisateur appuie sur la touche de tabulation, le contenu du champ est remplacé par le résultat actuellement sélectionné dans la liste, s'il y en a un ; sinon, le champ est simplement vidé. Ainsi, dans la situation présentée dans la figure 2 ci-dessus, si l'utilisateur appuyait sur la touche de tabulation, alors le contenu du champ de l'arrêt de départ serait remplacé par Lausanne.
2.2. Programme principal
Le programme principal n'est rien d'autre que la combinaison des trois parties de l'interface graphique développées dans les étapes 9 à 11. Ces différentes parties sont toutefois reliées entre elles de manière à ce que le changement des paramètres de recherche provoque la mise à jour de la liste des voyages et/ou du voyage sélectionné, et que la vue détaillée présente toujours le voyage actuellement sélectionné dans la vue d'ensemble.
La vidéo ci-dessous, obtenue avec les données horaires de la semaine 19, illustre le fonctionnement du programme une fois terminé.
Pour faciliter la compréhension, les touches tapées au clavier sont affichées brièvement au bas de la vidéo, tandis que les clics de la souris sont visualisés au moyen de cercles rouges. Ces éléments ne font bien entendu pas partie de l'interface graphique de ReCHor.
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 query.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 StopField
L'enregistrement StopField
du sous-paquetage gui
, public, représente la combinaison d'un champ textuel et d'une fenêtre, qui permet de choisir un arrêt de transport public. Il possède deux attributs, qui sont :
- le champ textuel, nommé p. ex.
textField
et de typeTextField
, - une valeur observable contenant le nom de l'arrêt sélectionné, nommée p. ex.
stopO
et de typeObservableValue<String>
.
La valeur observable doit obligatoirement contenir un nom d'arrêt valide, ou une chaîne vide si aucun arrêt ne correspond à la requête. Sa valeur ne doit changer que lorsque le champ textuel devient inactif, comme décrit dans les conseils de programmation plus bas.
StopField
possède une méthode publique et statique, nommée p. ex. create
, dont le but est de créer le champ textuel et la fenêtre et de retourner une instance de StopField
. Cette méthode prend en argument l'index des arrêts, de type StopIndex
, à utiliser pour rechercher les arrêts.
En plus de cette méthode statique, StopField
possède une méthode publique nommée p. ex. setTo
, qui prend en argument un nom d'arrêt, de type String
, et qui fait en sorte que ce nom soit celui associé au champ.
La mise en œuvre de StopField
étant assez subtile, il est important de bien lire les conseils de programmation qui suivent avant de la commencer.
3.1.1. Conseils de programmation
- Fenêtre contenant les résultats
La fenêtre contenant les résultats de la requête est une instance de
Popup
dont on a désactivé la disapparition lorsque la touche d'échappement est pressée, au moyen desetHideOnEscape
.Son contenu, défini en modifiant la liste retournée par
getContent
, est constitué d'une unique instance deListView<String>
qui montre les résultats de la requête. Cette instance deListView
est configurée de manière à ce qu'elle ne soit pas « traversable » (avecsetFocusTraversable
) et que sa hauteur maximale soit de 240 unités (avecsetMaxHeight
). - Valeur observable contenant le nom de l'arrêt
Comme cela a été dit ci-dessus, la valeur observable contenant le nom de l'arrêt ne doit changer que lorsque le champ textuel devient inactif, et elle doit contenir soit le nom de l'arrêt sélectionné, soit une chaîne vide si aucun arrêt ne correspond à la requête.
Ces conditions interdisent l'utilisation directe de la propriété
textProperty
du champ textuel comme valeur observable. Il faut donc créer une propriété séparée et la modifier de manière à respecter les contraintes susmentionnées. - Champ textuel
Le champ textuel contenant la requête (lorsqu'il est actif) ou le nom de l'arrêt sélectionné (lorsqu'il est inactif) doit être configuré de deux manières.
D'une part, un gestionnaire d'événement doit lui être ajouté, au moyen de
addEventHandler
, afin de détecter les pressions (KEY_PRESSED
) sur les touches du curseur permettant le déplacement vers le haut (UP
) et vers le bas (DOWN
). Lorsqu'un tel événement est détecté, et que la sélection de la liste des arrêts peut être déplacée vers l'élément précédent (pourUP
) ou suivant (pourDOWN
), alors elle doit l'être, et l'événement doit être consommé (consume
) afin de ne pas être traité par le gestionnaire par défaut du champ textuel — ce qui déplacerait le curseur.D'autre part, la propriété
focusedProperty
de ce champ doit être observée afin de déterminer quand le champ est actif (focused) ou non.Lorsque le champ devient actif, la fenêtre contenant les résultats doit devenir visible (
show
), et deux valeurs observables doivent alors être observées :- la propriété
textProperty
du champ textuel, afin de détecter les changements de la requête et mettre à jour la liste des résultats en fonction, - la propriété
boundsInLocalProperty
du champ textuel, dont la valeur doit être d'abord transformée dans le système de coordonnées de l'écran au moyen delocalToScreen
puis utilisée pour ancrer — c.-à-d. positionner —, au moyen desetAnchorX
etsetAnchorY
, la fenêtre contenant les résultats pour qu'elle se trouve alignée à gauche et juste sous le champ textuel.
Lorsque le champ devient inactif, les deux valeurs observables susmentionnées ne doivent plus être observées, la fenêtre montrant les résultats doit être cachée, et le nom de l'arrêt actuellement sélectionné doit être copié dans le champ textuel et dans la propriété décrite ci-dessus.
- la propriété
3.2. Enregistrement QueryUI
L'enregistrement QueryUI
du sous-paquetage gui
, public, représente « l'interface de requête », c.-à-d. la partie de l'interface graphique qui permet à l'utilisateur de choisir les arrêts de départ et d'arrivée, et la date/heure de voyage désirés. Il possède cinq attributs, qui sont :
- le nœud JavaFX à la racine de son graphe de scène, nommé p. ex.
rootNode
et de typeNode
, - une valeur observable contenant le nom de l'arrêt de départ, nommé p. ex.
depStopO
et de typeObservableValue<String>
, - une valeur observable contenant le nom de l'arrêt d'arrivée, nommé p. ex.
arrStopO
et de typeObservableValue<String>
, - une valeur observable contenant la date de voyage, nommé p. ex.
dateO
et de typeObservableValue<LocalDate>
, - une valeur observable contenant l'heure de voyage, nommé p. ex.
timeO
et de typeObservableValue<LocalTime>
.
Comme toutes les classes représentant une partie de l'interface graphique, QueryUI
offre une méthode publique et statique, nommée p. ex. create
, dont le but est de créer le graphe de scène de l'interface de requête et de retourner une instance de QueryUI
contenant, entre autres, le nœud JavaFX qui se trouve à sa racine. Cette méthode prend en argument un index de nom d'arrêts, de type StopIndex
, qui est utilisé pour rechercher les noms d'arrêts.
3.2.1. Graphe de scène
Le graphe de scène de l'interface de requête est présenté dans la figure ci-dessous.

Les instances de Label
visibles dans cette figure correspondent aux étiquettes textuelles qui se trouvent à gauche des différents champs. Le texte qui leur est associé est :
Départ :
pour le champ de l'arrêt de départ,Arrivée :
pour le champ de l'arrêt d'arrivée,Date :
pour le champ de la date de voyage,Heure :
pour le champ de l'heure de voyage.
Notez que, conformément aux règles typographiques suisses, les deux-points dans ces chaînes sont précédés d'une espace fine insécable, dont le code Unicode est 202F16.
En Java, un moyen simple d'intégrer une espace fine insécable à une chaîne est d'utiliser la séquence d'échappement Unicode \u202f
. Ainsi, la chaîne pour le champ de l'arrêt de départ peut s'écrire :
"Départ\u202f:"
3.2.2. Conseils de programmation
- Invites
Comme expliqué dans l'introduction, les champs textuels permettant de choisir les arrêts de départ et d'arrivée contiennent un texte d'invite (prompt text) lorsqu'ils sont vides. Ce texte peut leur être attaché au moyen de la méthode
setPromptText
. - Formatage de l'heure
Contrairement aux autres champs textuels, celui permettant d'entrer l'heure de départ ne contient pas à proprement parler une chaîne, mais une valeur de type
LocalTime
représentée sous forme de chaîne. Dès lors, la valeur contenue dans ce champ doit être transformée en chaîne au moment de l'affichage, et la chaîne entrée par l'utilisateur transformée en valeur de typeLocalTime
.JavaFX permet de faire ce genre de transformations relativement facilement au moyen d'un formateur de texte de type
TextFormatter
attaché au champ textuel. Pour celui dont il est question ici, ce formateur peut être une valeur de typeLocalTimeStringConverter
configurée de manière à ce que les heures soient toujours affichées avec deux chiffres — p. ex. 09:30 pour 9h30 — mais qu'il soit possible de les entrer avec un chiffre seulement — p. ex. 9:30. Pour ce faire, il convient de passer deux instances deDateTimeFormatter
différentes au constructeur deLocalTimeStringConverter
: la première utilisée pour transformer une heure en chaîne, la seconde utilisée pour transformer une chaîne en une heure.Lorsqu'un formateur de texte est ainsi attaché à un champ textuel, la propriété à utiliser pour obtenir ou modifier la valeur non textuelle, ici de type
LocalTime
, est la propriétévalueProperty
du formateur.
3.3. Classe Main
La classe Main
, du sous-paquetage gui
, instanciable, est la classe principale du projet. Comme toute classe principale d'une application utilisant JavaFX, elle hérite de Application
et fournit une mise en œuvre de sa méthode start
. Elle possède de plus une méthode main
qui ne fait rien d'autre qu'appeler launch
en lui passant les arguments reçus.
La redéfinition de la méthode start
a pour tâche de :
- charger les données horaires, qui doivent se trouver dans un sous-dossier nommé
timetable
du dossier courant au moment de l'exécution du programme, - construire l'interface graphique principale en combinant les parties créées par les classes
DetailUI
,SummaryUI
etQueryUI
, au moyen du graphe de scène présenté plus bas.
Les trois parties de l'interface doivent être reliées entre elles au moyen des valeurs observables qu'elles exportent ou prennent en argument. En particulier, la valeur observable contenant la liste des voyages à montrer doit être calculée à partir des valeurs observables de l'interface de requête, selon la technique décrite dans les conseils de programmation plus bas.
3.3.1. Graphe de scène
Le graphe de scène du programme principal est présenté dans la figure ci-dessous. L'instance de Scene
se trouvant à la racine de ce graphe est celle à utiliser comme « scène » principale du programme, c.-à-d. qu'elle doit être attachée à la fenêtre principale de l'application, passée à la méthode start
.

3.3.2. Conseils de programmation
- Chargement des données horaires
Pour que les données horaires soient chargées depuis le sous-dossier
timetable
du dossier courant, il faut passer à la méthodein
deFileTimeTable
le chemin d'accès suivant :Path.of("timetable")
- Voyages observables
Un des buts principaux de la méthode
start
est de créer la valeur observable de typeObservableValue<List<Journey>>
, qui contient les voyages à afficher et qui doit être passée à la vue d'ensemble des voyages. Cette valeur observable dépend bien entendu des valeurs observables exportées par l'interface de recherche, à savoir : l'arrêt de départ, l'arrêt d'arrivée et la date du voyage.En JavaFX, une valeur observable dont la valeur dépend de plusieurs autres valeurs observables peut être définie relativement aisément au moyen de la méthode
createObjectBinding
deBindings
. Cette méthode prend en premier argument une lambda qui ne prend pas d'arguments mais calcule la valeur observable, et ensuite toutes les dépendances, c.-à-d. les valeurs observables utilisées dans ce calcul.Par exemple, pour créer une valeur observable
o3
dont le contenu est la concaténation de deux chaînes observableso1
eto2
, on peut procéder ainsi :ObservableValue<String> o1 = …; ObservableValue<String> o2 = …; ObservableValue<String> o3 = Bindings.createObjectBinding( () -> o1.getValue() + o2.getValue(), o1, o2);
La valeur observable contenant la liste des voyages peut être définie de manière similaire, en prenant toutefois garde à deux points importants.
Premièrement, pour des raisons de performances, le profil correspondant à la requête doit être gardé dans un cache afin de ne pas être recalculé lorsque ni la date de voyage ni l'arrêt d'arrivée ne changent.
Deuxièmement, la valeur observable retournée par
createObjectBinding
doit impérativement être stockée dans un attribut de la classeMain
et pas seulement dans une variable locale de la méthodestart
, faute de quoi elle ne fonctionnera pas correctement1. - Configuration de la fenêtre principale de l'application
La fenêtre principale de l'application, passée à la méthode
start
sous la forme d'une valeur de typeStage
, doit être configurée de manière à ce que sa taille minimale soit de 800×600 unités (setMinWidth
etsetMinHeight
), et que son titre soitReCHor
(setTitle
).Une fois que la fenêtre principale a été rendue visible au moyen de la méthode
show
, le champ textuel correspondant à l'arrêt de départ doit être sélectionné. Cela peut se faire au moyen du code suivant :Platform.runLater(() -> scene.lookup("#depStop").requestFocus());
4. Tests
Étant donné que la classe représentant le programme principal doit absolument avoir le bon nom et posséder une méthode main
ayant la bonne signature, nous vous fournissons un fichier de vérification de signature pour cette étape. Il doit être intégré à votre projet comme ceux des étapes 1 à 6. Notez toutefois que les anciens fichiers de vérification de signature ne sont plus nécessaires, et peuvent être supprimés.
Pour tester votre programme, le plus simple est de l'utiliser pour effectuer des requêtes et de comparer les résultats obtenus avec ceux de sites comme celui des CFF. Notez qu'il est possible que vous obteniez des voyages différents, mais tant que les vôtres sont valides et ne sont pas dominés par ceux proposés par les CFF, cela ne pose pas de problème.
5. Résumé
Pour cette étape, vous devez :
- écrire les classes
StopField
,QueryUI
etMain
selon les indications plus haut, - tester votre code,
- documenter la totalité des entités publiques que vous avez définies,
- rendre votre projet au plus tard le 30 mai 2025 à 18h00, au moyen du programme
Submit.java
fourni et des jetons disponibles sur votre page privée.
Ce rendu est le rendu final, auquel un total de 110 points sont attribués — 90 par lecture du code de vos étapes 7 à 11, et 20 par test de votre programme.
N'attendez surtout pas le dernier moment pour effectuer votre rendu, car vous n'êtes pas à l'abri d'imprévus.
Si vous manquez la date limite de rendu, vous avez encore la possibilité de faire un rendu tardif au moyen des jetons prévus à cet effet, et ce durant les 2 heures qui suivent, mais il vous en coûtera une pénalité inconditionnelle de 11 points.
Notes de bas de page
La raison pour laquelle cela est nécessaire est que JavaFX stocke les observateurs d'une valeur observable au moyen de ce que l'on nomme des références faibles (weak references). En Java, lorsqu'un objet n'est référencé que par des références faibles, il est automatiquement détruit par le ramasse-miettes (garbage collector). Dans notre cas, cela implique que la valeur observable cesse de se mettre à jour. La décision des concepteurs de JavaFX d'utiliser des références faibles est une (grosse) erreur, mais nous devons malheureusement faire avec.