Programme principal

JaVelo – étape 11

1. Introduction

Le but de cette dernière étape obligatoire est de terminer le projet en écrivant les parties manquantes de l'interface graphique ainsi que le programme principal.

2. Concepts

2.1. Interface finale

L'interface finale de JaVelo est constituée d'une unique fenêtre montrant le fond de carte OpenStreetMap, qui peut être déplacé et dont l'échelle peut être changée. Au démarrage, la carte est affichée au niveau de zoom 12, et les coordonnées du coin haut-gauche, dans le système de coordonnées de la carte du monde entier, sont (x = 543200, y = 370650).

Chaque clic sur le fond de carte provoque l'ajout d'un nouveau point de passage après les éventuels déjà existants. La position d'un point peut être changée en cliquant sur lui puis en déplaçant la souris tout en maintenant le bouton enfoncé, avant de le relâcher à l'endroit désiré. Un clic sans déplacement provoque la suppression du point de passage. Lorsqu'un point de passage ne peut pas être ajouté ou déplacé à un endroit donné car aucune route ne se trouve à proximité, un message d'erreur apparaît temporairement à l'écran.

Dès qu'au moins deux points de passage sont présents, le meilleur itinéraire les reliant est calculé et affiché, sauf s'il est impossible de relier au moins deux des points successifs. Lorsqu'un itinéraire est affiché, son profil l'est également dans la partie inférieure de la fenêtre.

Une position le long de l'itinéraire peut être mise en évidence, et lorsque c'est le cas, elle l'est simultanément sur la carte — au moyen d'un disque rouge et blanc — et sur le profil — au moyen d'une ligne noire. La position mise en évidence est déterminée par celle du pointeur de la souris :

  • s'il survole la carte et se trouve à moins de 15 pixels de l'itinéraire, alors la position de l'itinéraire la plus proche de lui est mise en évidence,
  • s'il survole le graphe du profil, alors la position correspondante de l'itinéraire est mise en évidence.

Un clic sur le disque rouge et blanc de mise en évidence provoque l'ajout d'un nouveau point de passage entre les points voisins de la position qu'il représente.

Lorsqu'un itinéraire existe, il peut être exporté au format GPX en sélectionnant l'unique entrée du menu Fichier, qui se nomme Exporter GPX. L'itinéraire est exporté dans un fichier nommé javelo.gpx, placé dans le répertoire courant de l'application.

La vidéo ci-dessous illustre les principales fonctionalités de l'interface décrites à l'instant. Les données utilisées sont celles de la Suisse occidentale (ch_west), mais le répertoire les contenant a été renommé javelo-data conformément aux instructions de la §3.4.

3. Mise en œuvre Java

3.1. Correction d'erreurs

Avant d'entamer cette étape, il convient de corriger certaines erreurs qui ont été faites durant les étapes précédentes.

3.1.1. Gestion du zoom

Dans la §3.2.1.4 de l'étape 8, nous vous avions demandé d'ajouter directement la valeur retournée par la méthode getDeltaY au niveau de zoom courant lorsque la molette de la souris était utilisée. Cette solution ne fonctionne toutefois pas bien sur tous les systèmes, ni avec un trackpad. Pour cette raison, nous vous demandons d'utiliser la solution présentée ci-dessous, qui d'une part ne tient compte que du signe de la valeur retournée par getDeltaY, en ignorant les valeurs nulles, et qui d'autre part ne change pas le niveau de zoom s'il a déjà été changé durant les dernières 200 ms.

Pour faciliter votre travail, le code de cette solution vous est donné ci-dessous. Il calcule la valeur zoomDelta qui doit être ajoutée au niveau de zoom actuel pour calculer le nouveau.

SimpleLongProperty minScrollTime = new SimpleLongProperty();
pane.setOnScroll(e -> {
    if (e.getDeltaY() == 0d) return;
    long currentTime = System.currentTimeMillis();
    if (currentTime < minScrollTime.get()) return;
    minScrollTime.set(currentTime + 200);
    int zoomDelta = (int) Math.signum(e.getDeltaY());
    // … reste du code
  });

Notez qu'il est possible que ce code ne fonctionne pas exactement comme vous le désirez sur votre ordinateur, mais il ne vous faut pas pour autant l'adapter, car cela pourrait vous faire perdre des points lors du test de votre projet.

3.1.2. Points de passage dupliqués

Dans la §3.2.1.3 de l'étape 9, nous vous avions demandé d'interdire la création d'un point de passage intermédiaire pour un nœud pour lequel il en existait déjà un. Or aucune vérification similaire n'avait été demandée lors de l'ajout de points de passage en fin d'itinéraire, ou lors du déplacement de l'un d'entre eux, ce qui n'est pas cohérent. De plus, interdire les points de passage répétés est trop restrictif en soi, car cela empêche par exemple les itinéraires en boucle.

Sachant que seuls les points de passage consécutifs attachés au même nœud sont problématiques — car ils provoquent une exception lors du calcul de l'itinéraire — nous vous proposons de modifier le comportement du programme ainsi :

  1. aucune restriction n'est placée sur la création ou le déplacement des points de passage, tant et aussi longtemps qu'un nœud peut être trouvé dans leur voisinage,
  2. lors du calcul de l'itinéraire, et à ce moment-là seulement, le cas où deux points de passage successifs sont associés au même nœud est détecté et aucune tentative de calcul d'itinéraire n'est faite pour ce segment.

Ce changement est relativement simple à apporter, mais pose un problème : dès que deux points de passage successifs sont associés au même nœud, la correspondance qui existait jusqu'alors entre index de point de passage et index de segment d'itinéraire est perdue. Il n'est donc plus possible d'utiliser directement la méthode indexOfSegment de Route dans le gestionnaire du clic sur le disque de mise en évidence pour savoir à quel index ajouter le nouveau point de passage.

Pour résoudre ce problème, il convient d'ajouter une variante de cette méthode à RouteBean, qui prend en argument une position le long de l'itinéraire et retourne l'index du segment la contenant, en ignorant les segments vides. Son code n'étant pas totalement trivial, il vous est fourni ici :

public int indexOfNonEmptySegmentAt(double position) {
  int index = route().indexOfSegmentAt(position);
  for (int i = 0; i <= index; i += 1) {
    int n1 = waypoints.get(i).nodeId();
    int n2 = waypoints.get(i + 1).nodeId();
    if (n1 == n2) index += 1;
  }
  return index;
}

Dans ce code, waypoints est la liste des points de passage.

Notez qu'une fois que vous aurez terminé d'apporter les changements susmentionnés, le constructeur de la classe RouteManager n'aura plus besoin de recevoir un consommateur d'erreur en argument, et il faut donc le supprimer, de même que l'attribut correspondant de la classe.

3.2. Classe AnnotatedMapManager

La classe AnnotatedMapManager du sous-paquetage gui, publique et instanciable (donc finale), gère l'affichage de la carte « annotée », c.-à-d. le fond de carte au-dessus duquel sont superposés l'itinéraire et les points de passage.

L'unique constructeur public de cette classe prend en arguments :

  • le graphe du réseau routier, de type Graph,
  • le gestionnaire de tuiles OpenStreetMap, de type TileManager,
  • le bean de l'itinéraire, de type RouteBean,
  • un « consommateur d'erreurs » permettant de signaler une erreur, de type Consumer<String>.

Grâce à ces arguments, le constructeur crée un gestionnaire de fond de carte (BaseMapManager), un gestionnaire de points de passage (WaypointsManager) et un gestionnaire d'itinéraire (RouteManager) et combine, par empilement, leurs panneaux respectifs.

En dehors du constructeur, la classe AnnotatedMapManager offre deux méthodes publiques, qui sont :

  • une méthode, nommée p. ex. pane, retournant le panneau contenant la carte annotée,
  • une méthode, nommée p. ex. mousePositionOnRouteProperty, retournant la propriété contenant la position du pointeur de la souris le long de l'itinéraire.

La propriété retournée par mousePositionOnRouteProperty contient la position, le long de l'itinéraire, du point le plus proche du pointeur de la souris, ssi la distance séparant ce point du pointeur est inférieure ou égale à 15 unités JavaFX (pixels). Sinon, elle contient NaN.

3.2.1. Conseils de programmation

  1. Hiérarchie JavaFX

    La hiérarchie JavaFX de la carte annotée est très simple, puisqu'elle consiste en un simple empilement des panneaux contenant le fond de carte, l'itinéraire et les points de passage — dans cet ordre. Cet empilement se fait facilement au moyen d'une instance de StackPane dont les fils sont les trois panneaux, et à laquelle la feuille de style map.css est attachée.

  2. Propriétés internes

    La principale responsabilité de AnnotatedMapManager est de gérer la propriété contenant la position du pointeur de la souris le long de l'itinéraire. Cette position dépend de :

    • la position de la souris dans le panneau contenant la carte annotée,
    • les paramètres du fond de carte,
    • l'itinéraire.

    Les deux dernières de ces dépendances sont des propriétés JavaFX observables, par contre la position de la souris n'en est pas une, ce qui empêche malheureusement d'utiliser les mécanismes fournis par JavaFX (bindings et autres) pour garder à jour la position du pointeur de la souris le long de l'itinéraire.

    Afin que cela soit possible, nous vous recommandons donc de définir une propriété privée contenant la position actuelle de la souris, sous la forme d'une valeur de type Point2D. La valeur de cette propriété est mise à jour par les gestionnaires d'événements installés par setOnMouseMoved et setOnMouseExited. Le second d'entre eux est nécessaire pour détecter la sortie de la souris du panneau contenant la carte, qui doit provoquer la mise à NaN de la propriété contenant la position de la souris le long de l'itinéraire.

3.3. Classe ErrorManager

La classe ErrorManager du sous-paquetage gui, publique et instanciable (donc finale), gère l'affichage de messages d'erreur.

Elle possède un constructeur public dénué d'arguments, et offre deux méthodes publiques :

  • une méthode, nommée p. ex. pane, retournant le panneau, de type Pane, sur lequel apparaissent les messages d'erreur,
  • une méthode, nommée p. ex. displayError, prenant en argument une chaîne de caractères représentant un (court) message d'erreur et le faisant apparaître temporairement à l'écran, accompagné d'un son indiquant l'erreur.

3.3.1. Conseils de programmation

  1. Hiérarchie JavaFX

    La hiérarchie JavaFX utilisée pour l'affichage d'erreurs est très simple et consiste en un panneau de type VBox, auquel la feuille de style error.css est attachée, contenant un texte de type Text.

    Afin que le panneau affichant l'erreur ne bloque pas l'interaction avec l'interface graphique qui se trouve derrière lui, il doit être rendu transparent à la souris au moyen de la méthode setMouseTransparent.

  2. Son d'erreur

    Lorsqu'une erreur se produit, le message correspondant doit non seulement apparaître à l'écran, mais un son d'erreur doit également être émis. La manière la plus simple de faire cela en Java consiste à utiliser la méthode beep fournie par AWT, ainsi :

    java.awt.Toolkit.getDefaultToolkit().beep();
    
  3. Animation

    L'animation de l'apparition et de la disparition du panneau peut se faire au moyen des classes JavaFX suivantes :

    • FadeTransition, une animation qui change graduellement l'opacité d'un nœud JavaFX sur une durée déterminée,
    • PauseTransition, une animation qui ne fait rien,
    • SequentialTransition, une animation qui enchaîne séquentiellement d'autres animations.

    Une première instance de FadeTransition permet de faire passer l'opacité du panneau de 0 (totalement transparent) à 0.8 (presque opaque) sur une durée de 0.2 secondes. Ensuite, une instance de PauseTransition permet de laisser le panneau visible durant 2 secondes. Finalement, une seconde instance de FadeTransition permet de faire passer l'opacité du panneau de 0.8 à 0 sur une durée de 0.5 secondes.

    Une fois combinées en une seule instance de SequentialTransition, ces animations peuvent être jouées au moyen de la méthode play. Attention : une seule animation doit être active à la fois, donc si une nouvelle erreur est signalée alors que l'animation de la précédente n'est pas encore terminée, elle doit être interrompue explicitement au moyen de la méthode stop.

3.4. Classe JaVelo

La classe JaVelo du sous-paquetage gui, publique et instanciable (donc finale), est la classe principale de l'application. Comme toute classe représentant une application JavaFX, elle hérite de Application.

La méthode start de JaVelo se charge de construire l'interface graphique finale en combinant les parties gérées par les classes écrites précédemment et en y ajoutant le menu très simple présenté plus haut.

Les différents composants créés par la classe JaVelo doivent être configurés ainsi :

  • le graphe est chargé depuis le répertoire nommé javelo-data,
  • le cache disque de tuiles est stocké dans le répertoire nommé osm-cache,
  • l'hôte du serveur de tuiles est tile.openstreetmap.org,
  • la fonction de coût est une instance de CityBikeCF,
  • la taille minimale de la fenêtre est de 800×600 unités,
  • le titre de la fenêtre est JaVelo.

Attention : n'utilisez en aucun cas des valeurs différentes de celles susmentionnées dans votre projet, car cela pourrait faire planter votre programme au démarrage, ce qui entraînerait la perte de la totalité des points liés au test final.

3.4.1. Conseils de programmation

  1. Hiérarchie JavaFX

    La hiérarchie JavaFX de l'application finale consiste en un panneau de type BorderPane doté de deux fils :

    1. dans la zone centrale, une superposition du panneau contenant la carte et le profil (en arrière-plan) et du panneau contenant le message d'erreur (en avant-plan),
    2. dans la zone supérieure, une barre de menu très simple.

    Le panneau contenant le message d'erreur est généralement totalement transparent, donc invisible, sauf en cas d'erreur.

    Le panneau contenant la carte et le profil est une instance de SplitPane d'orientation verticale. Lorsqu'un itinéraire existe, ce panneau contient deux enfants qui sont, dans l'ordre : le panneau contenant la carte annotée, et le panneau contenant le dessin du profil. Lorsqu'aucun itinéraire n'existe, le second de ces enfants n'est pas présent dans le graphe de scène.

    Pour que le panneau contenant le profil ne soit pas redimensionné verticalement lorsque la fenêtre l'est, la méthode statique (!) setResizableWithParent peut être utilisée.

    La barre de menu est une instance de MenuBar, qui contient une seule instance de Menu (intitulée Fichier), qui contient elle-même une seule instance de MenuItem (intitulée Exporter GPX). Afin que l'exportation d'un itinéraire ne soit possible que lorsqu'il existe, la propriété disable de l'instance de MenuItem doit être liée à une expression qui n'est vraie que lorsque l'itinéraire est nul.

    L'action à effectuer lorsque le menu est sélectionné se définit au moyen de la méthode setOnAction. Dans un soucis de simplicité, l'itinéraire est toujours sauvegardé dans un fichier nommé javelo.gpx, et les éventuelles exceptions levées par la méthode writeGpx sont simplement levées à nouveau sous la forme d'exceptions de type UncheckedIOException.

  2. Position mise en évidence

    Une position peut être mise en évidence le long de l'itinéraire, simultanément sur la carte annotée et sur le profil.

    Pour ce faire, la propriété du bean de l'itinéraire contenant la position à mettre en évidence est liée à une valeur qui est égale :

    • à la position de la souris sur l'itinéraire, si celle-ci est supérieure ou égale à 0,
    • à la position de la souris sur le profile, sinon.

3.5. Tests

Étant donné que la classe représentant le programme principal doit impérativement avoir le bon nom et posséder une méthode main ayant la bonne signature, nous vous fournissons une archive Zip contenant 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.

4. Résumé

Pour cette étape, vous devez :

  • écrire les classes AnnotatedMapManager, ErrorManager et JaVelo selon les indications données ci-dessus,
  • tester votre code,
  • documenter la totalité des entités publiques que vous avez définies.
  • rendre votre projet dans le cadre du rendu final anticipé, si vous désirez tenter d'obtenir un bonus, ou dans le cadre du rendu final normal sinon ; les instructions concernant ces rendus seront publiées ultérieurement.

N'attendez surtout pas le dernier moment pour effectuer votre rendu, car vous n'êtes pas à l'abri d'imprévus. Souvenez-vous qu'aucun retard, aussi insignifiant soit-il, ne sera toléré !