Programme principal

Rigel – étape 11

1 Introduction

Le but de cette étape est de terminer le projet en écrivant la classe contenant le programme principal.

2 Concepts

2.1 Interface utilisateur

L'interface du programme Rigel est illustrée sur l'image ci-dessous.

rigel-final-gui;64.png
Figure 1 : Interface du programme Rigel (cliquez pour agrandir)

Cette interface est composée de trois parties principales qui, de haut en bas, sont :

  1. la « barre de contrôle », qui permet de régler les paramètres d'observation (lieu, instant), de choisir à quelle vitesse le temps simulé doit s'écouler, et de démarrer ou arrêter son écoulement,
  2. la vue du ciel, qui montre les objets célestes (étoiles, planètes, Lune et Soleil), les figures des astérismes, l'horizon et les points cardinaux et intercardinaux,
  3. la « barre d'information », qui indique le champ de vue, des informations concernant l'objet sous la souris et la position du curseur de la souris dans le système de coordonnées horizontal.

Le fonctionnement de ces différentes parties est décrit dans les sections suivantes, tandis que leur mise en œuvre en Java est décrite comme d'habitude à la §3.

2.1.1 Barre de contrôle

La barre de contrôle est composée de trois sous-parties, séparée les unes des autres par de fines barres verticales. De gauche à droite, ces parties permettent de définir : la position d'observation, l'instant d'observation, et l'écoulement du temps simulé.

  1. Position d'observation

    La position (géographique) d'observation est déterminée par les valeurs entrées dans les deux champs intitulés Longitude et Latitude.

    Ces valeurs sont spécifiées en degrés, avec deux décimales, et il est impossible d'entrer des valeurs invalides, c-à-d soit des valeurs qui ne sont pas des nombres, soit des nombres hors des limites.

    Au démarrage du programme, la longitude est de 6.57° et la latitude de 46.52°, une position qui se trouve sur le campus de l'EPFL.

  2. Instant d'observation

    L'instant d'observation, c-à-d la date, l'heure et le fuseau horaire d'observation, sont déterminés par les valeurs entrées dans les champs intitulés Date et Heure et dans le menu déroulant qui suit le champ contenant l'heure.

    Au démarrage du programme, l'instant d'observation est l'instant actuel, et le fuseau horaire est celui de l'ordinateur sur lequel le programme s'exécute.

  3. Écoulement du temps

    L'écoulement du temps, qui provoque l'animation du ciel, est contrôlé au moyen d'un menu déroulant et de deux boutons.

    Le menu déroulant permet de choisir l'accélération du temps parmi 6 possibilités, qui sont celles décrites à la §3.3 de l'étape 9. Au démarrage du programme, l'accélération sélectionnée est celle nommée 300×, qui correspond à un facteur d'accélération du temps réel de 300.

    Le premier bouton, dont l'image est une flèche circulaire (), permet de réinitialiser l'instant d'observation. Un clic sur ce bouton provoque la réinitialisation de l'instant d'observation à l'instant actuel.

    Le second bouton, dont l'image est le symbole play () si le temps est à l'arrêt ou le symbole pause () sinon, permet de démarrer ou d'arrêter l'écoulement du temps (simulé). Lorsque l'utilisateur clique sur ce bouton, l'écoulement du temps commence s'il est actuellement arrêté, et s'arrête sinon.

    Lorsque le temps s'écoule, les parties de l'interface qui permettent de contrôler l'instant d'observation ou les paramètres d'écoulement du temps sont désactivées, et donc inutilisables jusqu'à l'arrêt du temps.

2.1.2 Vue du ciel

La vue du ciel qui se trouve au centre de l'interface est celle décrite à l'étape 10.

La direction d'observation, c-à-d le centre de la projection stéréographique utilisée, est contrôlée par les touches du curseur lorsque le canevas affichant le ciel est le destinataire des événements clavier. Comme expliqué précédemment, les touches gauche et droite changent l'azimut de cette direction par pas de 10°, tandis que les touches haut et bas changent sa hauteur, par pas de 5°, en la restreignant à l'intervalle [5°, 90°].

Au démarrage du programme, l'azimut d'observation est de 180.000000000001° (plein sud)1 et la hauteur d'observation est de 15°, tandis que le champ de vue est de 100°.

2.1.3 Barre d'information

La barre d'information au bas de la fenêtre affiche les trois informations suivantes :

  • à gauche, le champ de vue, en degrés et avec une décimale,
  • au centre, les informations au sujet de l'objet le plus proche du curseur de la souris,
  • à droite, la position du curseur de la souris dans le système de coordonnées horizontal, en degrés et avec deux décimales pour chacune des composantes.

La partie centrale est vide si aucun objet n'est assez proche du curseur de la souris pour que ses informations soient affichées, mais les parties gauche et droite ne sont jamais vides.

3 Mise en œuvre Java

Attention : la classe Main décrite ci-dessous est la classe principale de votre programme, et il est donc capital qu'elle porte effectivement le nom donné, et qu'elle se trouve dans le paquetage ch.epfl.rigel.gui. Notre système de rendu refusera votre projet s'il ne contient pas cette classe, si elle est placée dans un autre paquetage, ou si elle est nommée différemment.

3.1 Classe Main

La classe Main du paquetage ch.epfl.rigel.gui contient le programme principal du projet. Comme toute classe représentant le programme principal d'une application JavaFX, elle hérite de la classe Application et sa méthode main ne fait qu'appeler la méthode launch.

À la racine du graphe de scène construit par la méthode start se trouve un panneau de type BorderPane, dont la zone supérieure est occupée par la barre de contrôle, la zone centrale par la vue du ciel, et la zone inférieure par la barre d'information.

La fenêtre principale de l'application, c-à-d l'instance de Stage passée à la méthode start, a pour titre Rigel, une largeur de 800 unités au minimum (minWidth), et une hauteur de 600 unités au minimum (minHeight).

Les autres éléments du graphe de scène sont décrits ci-dessous. Pour rendre votre code plus lisible, il est fortement conseillé de le découper en plusieurs méthodes privées, chargées chacune de construire une partie du graphe de scène, et appelées par la méthode start. L'organisation en sections de ce qui suit peut vous guider dans ce découpage.

3.1.1 Barre de contrôle

La barre de contrôle est un panneau horizontal de type HBox dont les trois enfants principaux sont eux aussi des panneaux de type HBox. Ces sous-panneaux sont séparés les uns des autres par un séparateur vertical de type Separator.

Le premier sous-panneau contient la partie de l'interface graphique permettant de régler la position d'observation, le second contient celle permettant de régler l'instant d'observation, et le troisième contient celle permettant de régler l'écoulement du temps.

Afin qu'elle ait l'aspect attendu, la barre de contrôle doit être configurée pour que ses enfants — les sous-panneaux et leurs séparateurs — soient correctement espacés et entourés d'une marge. Cette configuration pourrait se faire au moyen de différentes méthodes héritées de Pane ou Node, mais une solution plus simple existe : la méthode setStyle, qui prend en argument une chaîne de caractères décrivant le style du composant au moyen d'un sous-ensemble du langage CSS utilisé sur le Web.

Nous vous suggérons donc d'utiliser cette méthode, et nous vous donnons systématiquement ci-dessous les styles à appliquer aux différents nœuds graphiques. Ainsi, celui de la barre de contrôle est :

-fx-spacing: 4;
-fx-padding: 4;

ce qui signifie que vous devez appeler la méthode setStyle sur le panneau qui la représente, en lui passant en argument une chaîne contenant ce style, sur une seule ligne. En d'autres termes, vous devez écrire quelque chose comme :

HBox controlBar = /* … */;
controlBar.setStyle("-fx-spacing: 4; -fx-padding: 4;");

3.1.2 Position d'observation

Le premier sous-panneau de la barre de contrôle contient la partie de l'interface graphique permettant de définir la position d'observation. Il est de type HBox et son style est :

-fx-spacing: inherit;
-fx-alignment: baseline-left;

Les étiquettes décrivant le contenu des champs sont de type Label et le texte de la première est :

Longitude (°) :

Le texte de la seconde étiquette est similaire.

Les champs contenant la longitude et la latitude sont des instance de TextField dont le style est :

-fx-pref-width: 60;
-fx-alignment: baseline-right;

Afin que ces champs n'acceptent que des valeurs valides, c-à-d des nombres à deux décimales et compris dans les bons intervalles, il est nécessaire de leur attacher ce que JavaFX nomme un formateur de texte, de type TextFormatter.

Malheureusement, la manière dont cela doit être fait est complexe et mal documentée, raison pour laquelle nous vous donnons ci-dessous un exemple montrant comment créer le formateur permettant de valider la longitude (lonTextFormatter ci-dessous) et de l'attacher au champ textuel correspondant (lonTextField ci-dessous)

NumberStringConverter stringConverter =
  new NumberStringConverter("#0.00");

UnaryOperator<TextFormatter.Change> lonFilter = (change -> {
    try {
      String newText =
	change.getControlNewText();
      double newLonDeg =
	stringConverter.fromString(newText).doubleValue();
      return GeographicCoordinates.isValidLonDeg(newLonDeg)
	? change
	: null;
    } catch (Exception e) {
      return null;
    }
  });

TextFormatter<Number> lonTextFormatter =
  new TextFormatter<>(stringConverter, 0, lonFilter);

TextField lonTextField =
  new TextField();
lonTextField.setTextFormatter(lonTextFormatter);

Notez que le formateur lonTextFormatter possède une propriété nommée value et dont le contenu est la longitude en degrés, sous forme de valeur de type Double (!). C'est donc cette propriété qu'il faut lier à la propriété correspondante du bean contenant la position de l'observateur.

Le code ci-dessus peut facilement être adapté au cas de la latitude, et même placé dans une méthode séparée prenant en arguments les aspects qui diffèrent entre ces deux cas, afin d'éviter la duplication de code.

3.1.3 Instant d'observation

Le second sous-panneau de la barre de contrôle contient la partie de l'interface graphique permettant de définir l'instant d'observation, qui est la combinaison de la date d'observation, l'heure d'observation, et le fuseau horaire dans lequel le couple (date, heure) est exprimé.

Le sous-panneau est de type HBox et son style est :

-fx-spacing: inherit;
-fx-alignment: baseline-left;

Ses deux premiers fils sont consacrés à la date d'observation. Le premier d'entre eux est une étiquette explicative, de type Label, dont le texte est :

Date :

Le second d'entre eux est le nœud permettant de choisir effectivement la date d'observation, de type DatePicker. Sa propriété value est liée à la propriété date du bean contenant l'instant d'observation, et son style est :

-fx-pref-width: 120;

Les deux prochains fils du sous-panneau sont consacrés à l'heure d'observation. Le premier d'entre eux est une étiquette dont le texte est :

Heure :

tandis que le second est un champ textuel de type TextField ayant pour style :

-fx-pref-width: 75;
-fx-alignment: baseline-right;

Tout comme les champs textuels contenant la longitude et la latitude, celui contenant l'heure doit avoir un formateur de texte attaché afin que seules les heures valides puissent être entrées. Une fois encore, ce code vous est donné en raison de la piètre qualité de la documentation existante à ce sujet :

DateTimeFormatter hmsFormatter =
  DateTimeFormatter.ofPattern("HH:mm:ss");
LocalTimeStringConverter stringConverter =
  new LocalTimeStringConverter(hmsFormatter, hmsFormatter);
TextFormatter<LocalTime> timeFormatter =
  new TextFormatter<>(stringConverter);

Notez ici aussi que le formateur timeFormatter possède une propriété value dont le contenu est l'heure d'observation, sous la forme d'une valeur de type LocalTime. C'est donc elle qu'il faut lier avec l'heure d'observation du bean contenant l'instant d'observation.

Le cinquième et dernier fils du sous-panneau est consacré au fuseau horaire. Il s'agit d'un menu déroulant de type ComboBox dont le style est :

-fx-pref-width: 180;

La propriété value de ce menu déroulant est liée à la propriété zone du bean contenant l'instant d'observation. Les fuseaux horaires qu'il contient sont ceux dont le nom est retourné par la méthode getAvailableZoneIds de la classe ZoneId. Notez que sur la plupart des systèmes, il y en a plusieurs centaines. Pour faciliter la recherche, ces fuseaux doivent donc être triés par ordre alphabétique.

Lorsqu'une animation est en cours, tous les nœuds graphiques liés au choix de l'instant d'observation sont désactivés. Cela se fait facilement en liant leur propriété disable à la propriété running de l'animateur de temps.

3.1.4 Écoulement du temps

Le troisième sous-panneau de la barre de contrôle contient la partie de l'interface graphique permettant de définir l'écoulement du temps. Il s'agit encore une fois d'un panneau de type HBox, dont le style est :

-fx-spacing: inherit;

La manière dont le temps s'écoule est déterminée par un accélérateur de temps, dont le choix est effectué au moyen d'un menu déroulant de type ChoiceBox, qui est le premier fils du sous-panneau. Ce menu propose les six accélérateurs nommés définis à l'étape 9 dans le type énuméré NamedTimeAccelerator.

Les choix proposés par ce menu sont définis au moyen d'un appel à la méthode setItems, qui attend un objet de type ObservableList. Les méthodes de la classe FXCollections sont très utiles pour obtenir une telle liste à partir d'une liste Java « normale », de type java.util.List.

Notez de plus que la méthode select de la classe Bindings permet d'effectuer très facilement le lien entre la propriété accelerator de l'animateur de temps et la propriété value du menu. Cette méthode permet de sélectionner un attribut d'une propriété en fonction de son nom, et son utilisation est illustrée par le programme d'exemple ci-dessous, qu'il est conseillé d'étudier en détail :

ObjectProperty<NamedTimeAccelerator> p1 =
  new SimpleObjectProperty<>(NamedTimeAccelerator.TIMES_1);
ObjectProperty<String> p2 =
  new SimpleObjectProperty<>();

p2.addListener((p, o, n) -> {
    System.out.printf("old: %s  new: %s%n", o, n);
  });

p2.bind(Bindings.select(p1, "name"));
p1.set(NamedTimeAccelerator.TIMES_30);

Notez en particulier que la propriété p1 contient un accélérateur de temps nommé, tandis que la propriété p2 contient une chaîne de caractères. Il n'est donc pas possible de lier directement ces deux propriétés, car leurs types diffèrent.

Il est par contre possible de lier l'attribut name de la première propriété à la seconde, et c'est ce qui est fait à l'avant-dernière ligne au moyen de la méthode select. Cette méthode prend en second argument une chaîne de caractères, ici name, spécifiant l'attribut à extraire du contenu de la propriété passée en premier argument, et elle exige qu'un accesseur dont le nom commence par get existe pour cet attribut. Dans cet exemple, cela implique que la classe NamedTimeAccelerator doit posséder une méthode nommée getName retournant une valeur de type String pour que le programme fonctionne.

Cette sélection par nom n'est pas très propre, car les éventuelles erreurs ne sont signalées qu'à l'exécution du programme, mais fort pratique dans certains cas comme ici.

Le second et troisième fils du sous-panneau sont des boutons de type Button. Ces boutons affichent une image qui est en fait un caractère de la police mise à disposition gratuitement par le projet Font Awesome.

Afin de pouvoir utiliser cette police, il vous faut premièrement télécharger l'archive Zip que nous mettons à votre disposition, et placer le fichier qu'elle contient — nommé Font Awesome 5 Free-Solid-900.otf — dans le répertoire resources de votre projet. Cela fait, vous pouvez charger et utiliser la police ainsi :

InputStream fontStream = getClass()
  .getResourceAsStream("/Font Awesome 5 Free-Solid-900.otf");
Font fontAwesome = Font.loadFont(fontStream, 15);

Button resetButton = new Button("\uf0e2");
resetButton.setFont(fontAwesome);
// … reste du code

Des chaînes similaires à celle passée au constructeur de Button permettent d'obtenir les deux autres images. En résumé, les trois chaînes nécessaires à ce projet sont :

  • "\uf0e2" pour ,
  • "\uf04b" pour ,
  • "\uf04c" pour .

Notez que ces chaînes contiennent chacune un seul caractère Java, spécifié au moyen d'une séquence d'échappement Unicode. Pour mémoire, ces séquences d'échappement sont décrites à la §5 du cours sur les entrées/sorties et les valeurs à utiliser se trouvent sur les pages du site du projet Font Awesome dédiées aux icônes correspondantes, à savoir undo, play et pause.

Inspirez-vous du code ci-dessus pour charger la police Font Awesome mais ne le copiez pas bêtement dans votre projet sans le comprendre ou l'améliorer ! Par exemple, n'oubliez pas de fermer le flot correctement, donnez un nom parlant à la chaîne passée au constructeur de Button, etc.

3.1.5 Ciel

L'affichage du ciel est géré par la classe SkyCanvasManager de l'étape 10, et ne sera donc pas décrit à nouveau ici.

Notez toutefois que pour que le canevas sur lequel le ciel est dessiné puisse être correctement redimensionné, il est nécessaire de le placer dans un panneau de type Pane, placé lui-même dans la zone centrale du BorderPane à la racine du graphe de scène. Cela fait, les dimensions du canevas — largeur et hauteur —  peuvent être liées à celles du panneau.

Au démarrage du programme, le canevas doit être le destinataire des événements clavier, ce qui implique d'appeler sa méthode requestFocus après que la fenêtre principale ait été rendue visible. En d'autres termes, l'appel à requestFocus du panneau du ciel doit être fait après l'appel à show de la fenêtre principale.

3.1.6 Barre d'information

La barre d'information est un panneau de type BorderPane dont les zones gauche, centrale et droite sont occupées chacune par un texte de type Text. Le panneau lui-même a pour style :

-fx-padding: 4;
-fx-background-color: white;

Le texte de la zone de gauche a la forme suivante :

Champ de vue : <fov>°

<fov> donne le champ de vue, avec une décimale. Par exemple, si ce champ de vue vaut 87.6°, le texte de la zone de gauche est :

Champ de vue : 87.6°

Le texte de la zone centrale donne les informations au sujet de l'objet le plus proche du curseur de la souris, s'il y en a une distance de moins de 10 unités dans le repère du canevas. Ces informations sont celles obtenues via la méthode info de CelestialObject.

Le texte de la zone de droite a la forme suivante :

Azimut : <az>°, hauteur : <alt>°

<az> est l'azimut du point sous le curseur de la souris, avec deux décimales, tandis que <alt> est sa hauteur, avec deux décimales également.

Notez que la méthode format de la classe Bindings (!) peut être très utile pour définir le texte des zones de gauche et de droite. Il s'agit d'une variante de la méthode format de String que vous connaissez déjà, mais qui est dynamique dans le sens où on peut lui donner des liens ou propriétés en arguments et elle recalcule la chaîne formatée chaque fois que ces valeurs changent.

3.2 Tests

La vidéo ci-dessous illustre un exemple d'interaction avec le programme terminé, pour assister au dernier coucher de Soleil du semestre sur les bords de l'océan Atlantique.

4 Résumé

Pour cette étape, vous devez :

  • écrire la classe Main en fonction des indications données plus haut,
  • tester votre code,
  • documenter la totalité des entités publiques que vous avez définies,
  • rendre votre code au plus tard le 29 mai 2020 à 17h00, via le système de rendu.

Ce rendu est le rendu final, auquel 110 points sont attribués.

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é !

Notes de bas de page

1

La raison pour laquelle l'azimut choisi n'est pas exactement 180° est que cela permet d'éviter des erreurs qui peuvent se produire dans certains cas. Cette « solution » n'est ni très propre ni très satisfaisante, mais semble la moins pire à ce stade du projet.