Affichage du profil
JaVelo – étape 10
1. Introduction
Cette dixième et avant-dernière étape a pour but d'écrire la classe gérant l'affichage du profil en long de l'itinéraire, et l'interaction avec lui.
2. Concepts
2.1. Affichage du profil
Dans l'interface de JaVelo, la partie inférieure de la fenêtre affiche une représentation graphique du profil en long de l'itinéraire courant, ainsi que quelques statistiques le concernant – longueur, dénivelé, etc. Une grille est superposée au profil, et les lignes de cette grille étiquetées dans la marge.
La figure 1 ci-dessous montre comment le profil de l'itinéraire allant de l'EPFL à Sauvabelin, présenté à l'étape 6, est représenté par cette partie de l'interface.
Une position le long du profil peut être mise en évidence par une ligne verticale noire. Sur l'image ci-dessus, cette ligne se trouve à une position de 1500 m. Dans la version finale de l'interface, la position mise en évidence sur le profil au moyen de cette ligne est toujours identique à celle mise en évidence sur la carte au moyen du disque rouge et blanc.
Dans la figure ci-dessus, la grille est constituée de lignes verticales dessinées tous les kilomètres, et de lignes horizontales dessinées tous les 50 mètres. L'espacement utilisé entre les lignes dépend toutefois des caractéristiques du profil — longueur et plage d'altitudes couverte — et de la taille de sa représentation graphique.
Ainsi, les lignes verticales correspondant à la position peuvent être dessinées tous les
1, 2, 5, 10, 25, 50 ou 100 km, tandis que les lignes horizontales correspondant à l'altitude peuvent être dessinées tous les 5, 10, 20, 25, 50, 100, 200, 250, 500 ou 1000 m. La valeur utilisée dans les deux cas est la plus petite garantissant que, à l'écran, les lignes horizontales soient distantes d'au moins 25 unités JavaFX (pixels), et les verticales d'au moins 50 unités. (Si aucune valeur ne permet de garantir cela, la plus grande de toutes est utilisée.)
3. Mise en œuvre Java
La mise en œuvre de cette étape n'est pas excessivement compliquée, mais relativement longue. Nous vous conseillons donc de procéder petit à petit, en commençant par exemple par dessiner uniquement le profil lui-même — sans la grille ni la ligne de mise en évidence — avant d'ajouter les autres éléments.
3.1. Classe ElevationProfileManager
La classe ElevationProfileManager
du sous-paquetage gui
, publique et finale, gère l'affichage et l'interaction avec le profil en long d'un itinéraire. Elle possède un unique constructeur public prenant en arguments :
- une propriété, accessible en lecture seule, contenant le profil à afficher ; elle est de type
ReadOnlyObjectProperty<ElevationProfile>
et contientnull
dans le cas où aucun profil n'est à afficher, - une propriété, accessible en lecture seule, contenant la position le long du profil à mettre en évidence ; elle est de type
ReadOnlyDoubleProperty
et contientNaN
dans le cas où aucune position n'est à mettre en évidence.
En plus de ce constructeur, elle offre les méthodes publiques suivantes :
- une méthode, nommée p. ex.
pane
, qui retourne le panneau contenant le dessin du profil, - une méthode, nommée p. ex.
mousePositionOnProfileProperty
, qui retourne une propriété en lecture seule contenant la position du pointeur de la souris le long du profil (en mètres, arrondie à l'entier le plus proche), ouNaN
si le pointeur de la souris ne se trouve pas au-dessus du profil.
On considère que le pointeur de la souris se trouve au-dessus du profil s'il se trouve dans le rectangle bleu de la figure 3 plus bas.
Attention à ne pas confondre la propriété contenant la position à mettre en évidence, passée au constructeur, avec celle donnant la position du curseur de la souris au-dessus du profil, retournée par la méthode ci-dessus. Ces deux propriétés sont indépendantes et peuvent très bien avoir des valeurs différentes.
3.1.1. Conseils de programmation
- Hiérarchie JavaFX
La hiérarchie de l'affichage du profil étant plus complexe que celle des autres parties de l'interface, elle est présentée graphiquement dans la figure 2 ci-dessous.
Dans cette figure, les rectangles représentent des nœuds JavaFX, et les flèches qui les lient vont des parents aux enfants. Certains nœuds sont accompagnés d'une étiquette dont la couleur détermine la signification :
- en vert, les feuilles de style CSS à attacher au nœud en modifiant 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 modifiant la liste retournée par
getStyleClass
.
Les feuilles de cette hiérarchie sont les nœuds représentant les différents éléments graphiques de l'affichage du profil. De gauche à droite, il s'agit de :
- un chemin (de type
Path
) représentant la grille, - un certain nombre d'étiquettes textuelles (de type
Text
) regroupées dans un groupe (de typeGroup
) et représentant les étiquettes de la grille, - un polygone (de type
Polygon
) représentant le graphe du profil, - une ligne (de type
Line
) représentant la position mise en évidence, - un texte (de type
Text
) contenant les statistiques du profil.
Tous ces éléments sauf le dernier sont regroupés dans un panneau de type
Pane
, tandis que le dernier est placé dans un panneau de typeVBox
. Ces deux conteneurs sont placés respectivement dans la zone centrale et inférieure du panneau de typeBorderPane
à la racine de la hiérarchie. C'est ce dernier qui est retourné par la méthodepane
.Le polygone représentant le profil est composé d'un point par unité JavaFX dans sa partie supérieure, et de deux points additionnels dans sa partie inférieure, situés aux extrémités gauche et droite et à la position correspondant à l'altitude minimale du profil. Ces deux points additionnels ont pour but de rendre la partie inférieure du profil semi-rectangulaire.
- en vert, les feuilles de style CSS à attacher au nœud en modifiant la liste retournée par
- Rectangle contenant le profil
La plus grande partie du panneau montrant le dessin du profil est occupée par le profil lui-même, comme le montre la figure ci-dessous sur laquelle il est entouré d'un rectangle bleu.
La distance entre le bord supérieur du rectangle bleu et le bord supérieur du panneau le contenant est de 10 unités JavaFX (pixels), de même que la distance entre leurs deux bords droits ; la distance entre leurs bords inférieurs est de 20 unités ; et finalement, la distance entre leurs bords gauche est de 40 unités. Ces quatre distances peuvent être stockées dans une instance de la classe
Insets
de JavaFX, prévue pour cela, qui peut être créée ainsi :new Insets(10, 10, 20, 40)
Tout point se trouvant à l'intérieur de ce rectangle correspondent à un couple (position, altitude) dans le monde réel. Par exemple, le point dans le coin haut-gauche du rectangle de la figure 3 correspond au couple (0 m, 663 m). Dans le système de coordonnées JavaFX du panneau contenant le rectangle, ce même point a les coordonnées (40, 10).
Sachant qu'il est très fréquent de devoir convertir entre ces deux systèmes de coordonnées, nous vous conseillons très fortement de représenter le passage de l'un à l'autre au moyen d'une paire de valeurs de type
Transform
, le type JavaFX représentant ce que l'on nomme des transformation affines. Cette classe offre entre autres plusieurs variantes d'une méthode nomméetransform
permettant d'appliquer la transformation à un point, et plusieurs variantes d'une méthode nomméedeltaTransform
permettant de l'appliquer à un vecteur.La conversion entre les deux systèmes de coordonnées qui nous intéressent ici peut s'exprimer assez facilement en enchaînant une première translation, une mise à l'échelle, et une seconde translation.
Pour construire un tel enchaînement au moyen des classes offertes par JavaFX, il suffit de créer une instance de
Affine
et d'utiliser les méthodesprependTranslation
etprependScale
pour y ajouter les trois transformations, dans l'ordre donné ci-dessus. (Ces transformations sont appliquées dans l'ordre inverse, raison pour laquelle nous vous conseillons d'utiliser les méthodes dont le nom commence parprepend
, et pas celles dont le nom commence parappend
).Plutôt que de ne créer qu'une seule transformation, nous vous conseillons d'en créer deux, à savoir :
screenToWorld
, qui passe du système de coordonnées du panneau JavaFX contenant le rectangle bleu au système de coordonnées du « monde réel »,worldToScreen
, qui fait le passage inverse.
Bien entendu, la seconde transformation est simplement l'inverse de la première, et s'obtient très facilement à partir d'elle au moyen de la méthode
createInverse
deTransform
.Comme expliqué ci-dessous, nous vous conseillons de stocker ces deux transformations dans des propriétés à usage interne, afin de pouvoir facilement mettre à jour l'interface lorsqu'elles changent.
- Propriétés à usage interne
Afin de faciliter la mise à jour de l'interface graphique, il est recommandé de définir les propriétés suivantes, réservées à un usage interne de la classe :
- une propriété de type
ObjectProperty<Rectangle2D>
contenant le rectangle englobant le dessin du profil — le rectangle bleu de la figure 3, - deux propriétés de type
ObjectProperty<Transform>
contenant les transformationsscreenToWorld
etworldToScreen
.
L'existence de ces propriétés permet d'utiliser les mécanismes JavaFX basés sur le patron Observer — liens, auditeurs, etc. — pour mettre à jour votre interface, ce qui simplifie et clarifie le code. Par exemple, la ligne de mise en évidence peut ainsi être placée et rendue visible de manière très simple en liant, au moyen de la méthode
bind
, quatre de ses propriétés :layoutX
à une expression donnant la coordonnée x correspondant à la position à mettre en évidence, qui provient de la propriété passée au constructeur — expression qui peut être construite au moyen de la méthodecreateDoubleBinding
deBindings
,startY
à la valeur de la propriétéminY
du rectangle englobant le dessin du profil — qui s'obtient en appliquant la méthodeselect
deBindings
à la propriété contenant ce rectangle et à la chaîne"minY"
,endY
à la valeur de la propriétémaxY
du rectangle englobant le dessin du profil — qui s'obtient de manière similaire àminY
,visible
à une expression qui n'est vraie que si la propriété contenant la position à mettre en évidence contient une valeur supérieure ou égale à 0 — qui s'obtient au moyen de la méthodegreaterThanOrEqualTo
.
- une propriété de type
- Grille
La totalité de lignes horizontales et verticales composant la grille superposée au profil est représentée au moyen d'un seul nœud JavaFX de type
Path
. Chacune des lignes est composée de deux « éléments de chemin » de typePathElement
:- une instance de
MoveTo
, dont les coordonnées sont celles d'une extrémité de la ligne, - une instance de
LineTo
, dont les coordonnées sont celles de l'autre extrémité de la ligne.
Ces deux éléments de chemin peuvent être vus comme des instructions permettant de dessiner une ligne en deux étapes : premièrement, en se déplaçant à une des extrémités de la ligne (
MoveTo
) ; puis deuxièmement, en tirant un trait depuis là jusqu'à l'autre extrémité de la ligne (LineTo
).Le choix de l'espacement à utiliser entre les lignes de la grille est fait selon la technique décrite à la §2.1. Pour faciliter votre travail, les différentes valeurs utilisables pour séparer les lignes (verticales) de position et celles (horizontales) d'altitude vous sont fournies ici sous forme de tableaux Java que vous pouvez copier dans votre classe :
int[] POS_STEPS = { 1000, 2000, 5000, 10_000, 25_000, 50_000, 100_000 }; int[] ELE_STEPS = { 5, 10, 20, 25, 50, 100, 200, 250, 500, 1_000 };
Notez que les lignes de la grille ne doivent être placées qu'à des multiples de l'espacement choisi.
Pour déterminer la distance séparant deux lignes de la grille, vous pouvez utiliser la méthode
deltaTransform
de la transformationworldToScreen
. Cette méthode fonctionne de manière similaire àtransform
mais est destinée à transformer un vecteur plutôt qu'un point, et elle ignore donc l'éventuelle translation effectuée par la transformation. - une instance de
- Étiquettes
La valeur associée à chacune des lignes de la grille est donnée dans la marge du graphe, en bas pour la position, à gauche pour l'altitude.
Les étiquettes de position doivent être placées de manière à ce que leur texte soit centré sous la ligne correspondante. Pour ce faire, l'instance de
Text
les représentant doit être :- positionnée par rapport à son sommet, en définissant la valeur de sa propriété
textOrigin
àVPos.TOP
, - décalée à gauche par rapport à la position de la ligne d'une distance égale à la moitié de sa propre largeur, qui s'obtient en appelant la méthode
prefWidth
avec 0 en argument.
Notez de plus que ces étiquettes sont exprimées en kilomètres, bien qu'en interne la position soit représentée en mètres.
Les étiquettes d'altitude doivent être placées de manière à ce que leur texte soit justifié à droite et centré à côté de la ligne correspondantes. Pour ce faire, l'instance de
Text
les représentant doit être :- positionnée par rapport à son centre, en définissant la valeur de sa propriété
textOrigin
àVPos.CENTER
, - décalée à gauche par rapport à la position de la ligne d'une distance égale à sa propre largeur plus 2.
La classe de style
grid_label
doit être attachée à toutes les étiquettes, ainsi que la classehorizontal
pour celles associées à une position, etvertical
pour celles associées à une altitude.Pour que la méthode
prefWidth
retourne la bonne valeur, il est indispensable d'associer à chacune des instances deText
représentant une étiquette la bonne police de caractères, au moyen de la méthodesetFont
. Pour les étiquettes, nous utiliserons la police Avenir de taille 10, qui s'obtient ainsi :Font.font("Avenir", 10)
- positionnée par rapport à son sommet, en définissant la valeur de sa propriété
- Statistiques
Les statistiques de l'itinéraire présentées dans le bas du panneau sont constituées d'un texte qui peut être facilement construit au moyen de la méthode
format
(ouformatted
) deString
. La chaîne de formatage à utiliser peut être obtenue au moyen de l'expression suivante :"Longueur : %.1f km" + " Montée : %.0f m" + " Descente : %.0f m" + " Altitude : de %.0f m à %.0f m"
- Gestion des événements
ElevationProfileManager
doit détecter les mouvements de la souris au-dessus du rectangle contenant le dessin du profil afin de garder à jour le contenu de la propriétémousePositionOnProfileProperty
.Cela implique d'installer deux gestionnaires d'événements sur le panneau contenant le profil :
- un premier détectant les mouvements du pointeur de la souris lorsqu'elle survole ce panneau, installé au moyen de
setOnMouseMoved
, - un second détectant la sortie du pointeur de la souris du panneau, installé au moyen de
setOnMouseExited
.
Pour mémoire, lorsque le pointeur de la souris ne se trouve pas dans le rectangle contenant le profil, la propriété doit contenir
NaN
. - un premier détectant les mouvements du pointeur de la souris lorsqu'elle survole ce panneau, installé au moyen de
3.2. Tests
Pour tester votre classe, vous pouvez utiliser le programme ci-dessous, que vous devrez probablement adapter à votre projet. Notez l'appel à bind
utilisé pour lier la propriété contenant la position à mettre en évidence à celle contenant la position de la souris au-dessus du profil.
public final class Stage10Test extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) throws Exception { Graph graph = Graph.loadFrom(Path.of("lausanne")); CityBikeCF costFunction = new CityBikeCF(graph); RouteComputer routeComputer = new RouteComputer(graph, costFunction); Route route = routeComputer .bestRouteBetween(159049, 117669); ElevationProfile profile = ElevationProfileComputer .elevationProfile(route, 5); ObjectProperty<ElevationProfile> profileProperty = new SimpleObjectProperty<>(profile); DoubleProperty highlightProperty = new SimpleDoubleProperty(1500); ElevationProfileManager profileManager = new ElevationProfileManager(profileProperty, highlightProperty); highlightProperty.bind( profileManager.mousePositionOnProfileProperty()); Scene scene = new Scene(profileManager.pane()); primaryStage.setMinWidth(600); primaryStage.setMinHeight(300); primaryStage.setScene(scene); primaryStage.show(); } }
En exécutant ce programme, vous devriez voir apparaître à l'écran une fenêtre ayant le même aspect que celle de la figure 1.
En déplaçant le pointeur de la souris sur le profil vous devriez constater que la ligne de mise en évidence le suit.
En redimensionnant la fenêtre, vous devriez constater que le profil est également redimensionné, et qu'à certains moments l'espacement entre les lignes de la grille change.
4. Résumé
Pour cette étape, vous devez :
- écrire la classe
ElevationProfileManager
selon les indications données ci-dessus, - tester votre code,
- documenter la totalité des entités publiques que vous avez définies.
Aucun rendu n'est à faire pour cette étape avant le rendu final. N'oubliez pas de faire régulièrement des copies de sauvegarde de votre travail en suivant nos indications à ce sujet.