Affichage de l'itinéraire

JaVelo – étape 9

1. Introduction

Cette neuvième étape a pour but d'écrire les classes permettant de gérer l'affichage de l'itinéraire sur le fond de carte.

2. Concepts

Dans l'interface de JaVelo, l'itinéraire est représenté par une ligne rouge reliant les points de passage entre eux. Une position le long de cet itinéraire peut de plus être mise en évidence par un disque blanc à bord rouge.

Ces deux éléments sont visibles dans la copie d'écran ci-dessous qui montre un itinéraire allant de l'EPFL à Sauvabelin en passant par Lutry. La position située à 1000 m le long de l'itinéraire est mise en évidence.

gui-route-with-highlight;64.png
Figure 1 : Représentation d'un itinéraire avec une position mise en évidence

3. Mise en œuvre Java

3.1. Classe RouteBean

La classe RouteBean du sous-paquetage gui, publique et finale, est un bean JavaFX regroupant les propriétés relatives aux points de passage et à l'itinéraire correspondant. Son unique constructeur public prend en argument un calculateur d'itinéraire, de type RouteComputer, utilisé pour déterminer le meilleur itinéraire reliant deux points de passage.

Les propriétés publiques de RouteBean sont :

  • la liste (observable) des points de passage, nommée p. ex. waypoints, de type ObservableList<Waypoint>,
  • l'itinéraire permettant de relier les points de passage, nommé p. ex. route, qui est en lecture seule et donc de type ReadOnlyObjectProperty<Route>,
  • la position mise en évidence, nommée p. ex. highlightedPosition, de type DoubleProperty,
  • le profil de l'itinéraire, nommé p. ex. elevationProfile, qui est en lecture seule et donc de type ReadOnlyObjectProperty<ElevationProfile>.

Les seules propriétés modifiables depuis l'extérieur sont la liste des points de passage et la position mise en évidence. Les deux autres propriétés — l'itinéraire et son profil — sont en lecture seule car leur valeur est déterminée par la liste des points de passage.

Lorsque la liste des points de passage ne contient pas au moins 2 éléments, ou s'il existe au moins une paire de points de passage entre lesquels aucun itinéraire ne peut être trouvé, alors ni l'itinéraire ni son profil n'existent, et les propriétés correspondantes contiennent null.

La propriété contenant la position mise en évidence contient soit un nombre valide donnant cette position — exprimée en mètres depuis le départ de l'itinéraire — soit NaN dans le cas où aucune position ne doit être mise en évidence.

Comme tout bean JavaFX, RouteBean devrait normalement offrir plusieurs méthodes publiques pour chacune de ses propriétés. Par exemple, les méthodes suivantes devraient être fournies pour celle contenant la position mise en évidence :

  • une méthode nommée highlightedPositionProperty, retournant la propriété elle-même, de type DoubleProperty ;
  • une méthode nommée highlightedPosition, retournant le contenu de la propriété, de type double ;
  • une méthode nommée setHighlightedPosition, prenant une valeur de type double et la stockant dans la propriété.

Si nous suivions à la lettre les conventions des beans JavaFX, toutes ces méthodes devraient être définies pour chacune des propriétés, à l'exception de la dernière méthode qui ne devrait pas l'être pour les propriétés accessibles en lecture seule. Toutefois, vous verrez que nous n'utiliserons pas la totalité de ces méthodes dans le projet, et nous vous conseillons donc de les ajouter à votre bean au fur et à mesure que vous constatez leur utilité.

3.1.1. Conseils de programmation

  1. Propriétés en lecture seule

    Les propriétés contenant l'itinéraire et son profil ne doivent être accessibles qu'en lecture seule depuis l'extérieur du bean, car leur valeur est déterminée par la liste des points de passage.

    Toutefois, le bean lui-même doit pouvoir modifier le contenu de ces propriétés, et doit donc les stocker sous la forme d'attributs de type ObjectProperty<…>, et non pas ReadOnlyObjectProperty<…>.

    Cela n'est toutefois pas un problème, car ObjectProperty<…> est un sous-type de ReadOnlyObjectProperty<…>, et il est donc possible de :

    • stocker la propriété dans un attribut de type ObjectProperty<…>,
    • fournir une méthode d'accès à la propriété qui la retourne sous la forme d'une valeur de type ReadOnlyObjectProperty<…>.

    Bien entendu, rien n'empêche un utilisateur du bean de contourner cette protection en effectuant un transtypage de la valeur retournée par la méthode d'accès vers le type ObjectProperty<…>, puis en modifiant la valeur contenue dans la propriété. Cela n'est toutefois pas important, notre but ici étant de nous prévenir contre les erreurs d'inattention, et pas de nous protéger contre un adversaire mal intentionné.

  2. Calcul de l'itinéraire

    L'itinéraire et son profil doivent être recalculés chaque fois que les points de passage changent, ce qui peut se faire facilement en installant un auditeur sur la liste contenant ces derniers. Lors d'un changement, le meilleur itinéraire (simple) reliant chaque point de passage à son successeur est déterminé, et ces itinéraires sont combinés en un unique itinéraire multiple.

    Le calcul d'un itinéraire étant une opération coûteuse, il est toutefois important d'éviter le recalcul d'itinéraires déjà calculés. Pour ce faire, nous vous conseillons d'utiliser un petit cache mémoire représenté par une table associant à une paire de nœuds le meilleur itinéraire (simple) les reliant.

    Chaque fois qu'un itinéraire (simple) doit être calculé, la table est consultée, et si elle contient l'itinéraire, il en est simplement extrait. Sinon, il est calculé au moyen du calculateur d'itinéraire, et le résultat inséré dans la table.

    Bien entendu, il ne faut pas que la table grossisse de manière incontrôlée, et nous vous conseillons donc soit de n'y stocker que les itinéraires (simples) correspondant à l'itinéraire (multiple) courant, soit de limiter sa taille en procédant de la même manière que pour le cache mémoire des tuiles.

  3. Calcul du profil

    Le profil de l'itinéraire est bien entendu calculé au moyen de la classe ElevationProfileComputer écrite à l'étape 5. Afin d'obtenir un profil détaillé mais de taille raisonnable, la distance maximale entre deux échantillons que nous utiliserons est de 5 mètres.

3.2. Classe RouteManager

La classe RouteManager du sous-paquetage gui, publique et finale, gère l'affichage de l'itinéraire et (une partie de) l'interaction avec lui. Elle possède un unique constructeur public prenant en arguments :

  • le bean de l'itinéraire,
  • une propriété JavaFX, en lecture seule, contenant les paramètres de la carte affichée,
  • un « consommateur d'erreurs » permettant de signaler une erreur.

En plus de ce constructeur public, RouteManager n'offre qu'une seule méthode publique, nommée p. ex. pane, retournant le panneau JavaFX contenant la ligne représentant l'itinéraire et le disque de mise en évidence.

3.2.1. Conseils de programmation

  1. Hiérarchie JavaFX

    L'itinéraire lui-même est représenté par une instance de Polyline portant l'identité route, qu'on lui attache au moyen de la méthode setId. Polyline est la classe JavaFX représentant ce que l'on nomme parfois une polyligne, c.-à-d. une suite de segments de droite reliés entre eux. Une polyligne est caractérisée par une séquence de points, qui ici sont simplement les points aux extrémités des arêtes de l'itinéraire.

    Le disque représentant la position mise en évidence est représenté quant à lui par une instance de Circle d'un rayon de 5 unités et portant l'identité highlight.

    Ces deux éléments sont les seuls enfants du panneau contenant l'itinéraire, qui est une instance de Pane. Comme celui contenant les points de passage, ce panneau doit être configuré de manière à ne pas bloquer les événements des nœuds se trouvant derrière lui, en appelant la méthode setPickOnBounds avec false en argument.

    Il faut noter que la polyligne et le disque sont toujours présents dans le graphe de scène, mais il ne sont pas toujours visible. Par exemple, si aucun itinéraire n'existe — c.-à-d. si la propriété route du bean contient null — alors ni l'un ni l'autre ne sont visibles.

  2. Positionnement de l'itinéraire

    Afin de ne pas devoir recréer la totalité de la polyligne représentant l'itinéraire à chaque glissement de la carte, il convient de choisir judicieusement le système de coordonnées dans lequel les positions de ses points sont exprimées.

    Nous vous conseillons d'utiliser pour cela le système de coordonnées de l'image de la carte au niveau de zoom courant. Cela fait, pour que la polyligne apparaisse à la bonne position à l'écran, il suffit de la placer en changeant ses propriétés layoutX et layoutY en tenant compte des coordonnées du coin haut-gauche de la portion de la carte visible.

  3. Gestion des événements

    RouteManager ne doit détecter et gérer qu'un seul type d'événement, le clic sur le disque représentant la position mise en évidence, qui provoque l'ajout d'un nouveau point de passage. Ce point de passage est situé à l'endroit du clic, et le nœud qui lui est associé est le nœud de l'itinéraire le plus proche de la position mise en évidence. S'il existe déjà un point de passage pour ce nœud, alors aucun point n'est ajouté, et l'erreur suivante est signalée :

    Un point de passage est déjà présent à cet endroit !
    

    Notez que pour déterminer les coordonnées du clic, vous ne pouvez pas comme d'habitude utiliser uniquement les méthodes getX et getY de l'événement souris, car les valeurs qu'elles retournent sont exprimées dans le système de coordonnées du nœud source, ici le disque représentant la position mise en évidence. Il faut donc transformer ces coordonnées dans le système du panneau contenant ce disque, ce qui se fait facilement au moyen de la méthode localToParent.

    En plus de ce gestionnaire d'événements, RouteManager doit mettre en place des auditeurs JavaFX afin de :

    • positionner et/ou rendre (in)visible le disque indiquant la position mise en évidence lorsque celle-ci change, lorsque la route change, ou lorsque les paramètres de la carte changent,
    • reconstruire totalement et/ou rendre (in)visible la polyligne représentant l'itinéraire lorsque ce dernier change, ou lorsque le niveau de zoom de la carte change,
    • repositionner — sans la reconstruire — la polyligne représentant l'itinéraire lorsque la carte a été glissée mais que son niveau de zoom n'a pas changé.

    Pour différencier les deux derniers cas, souvenez-vous que les auditeurs attachés au moyen de addListener reçoivent trois arguments qui sont, dans l'ordre : la propriété qui a changé, son ancienne valeur, et sa nouvelle valeur. Il est donc facile d'obtenir les anciens et nouveaux paramètres de la carte afin de déterminer si le niveau de zoom a changé ou non.

    La visibilité d'un nœud JavaFX est déterminée par sa propriété visible, qui peut être changée au moyen de la méthode setVisible.

3.3. Tests

Pour tester cette étape, vous pouvez augmenter le programme de test de l'étape précédente afin de :

  1. créer une instance de RouteBean,
  2. remplacer la liste de points de passage (waypoints) par celle de cette instance de RouteBean,
  3. forcer la valeur de la propriété highlightedPosition de ce bean à une valeur constante quelconque, p. ex. 1000 m,
  4. créer une instance de RouteManager,
  5. ajouter le panneau de cette instance de RouteManager comme dernier fils de la StackPane nommée mainPane.

Une fois ces changements effectués, votre programme de test devrait se comporter comme illustré dans la vidéo ci-dessous.

4. Résumé

Pour cette étape, vous devez :

  • écrire les classes RouteBean et RouteManager 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.