Gestion du canevas

Rigel – étape 10

1 Introduction

Le but principal de cette étape est d'écrire le code assurant la gestion du canevas sur lequel le ciel est dessiné.

Cela consiste d'une part à gérer l'interaction avec l'utilisateur, en lui offrant la possibilité de changer la direction d'observation et le niveau de zoom ; et d'autre part à suivre les mouvements de la souris afin de connaître à chaque instant les coordonnées du point situé sous son curseur, ainsi que l'objet céleste qui en est le plus proche. Ces deux informations seront affichées plus tard au bas de la fenêtre du programme.

2 Concepts

Pour choisir la portion du ciel à dessiner, l'utilisateur du programme Rigel aura la possibilité de changer deux paramètres, qui sont le champ de vue (un angle) et le centre de projection (une position dans le repère horizontal). Il convient donc de décrire ces deux paramètres et la manière dont ils pourront être contrôlés.

2.1 Champ de vue

Le champ de vue (field of view, souvent abrégé FoV en anglais) d'une image du ciel est l'angle séparant son bord gauche de son bord droit, au niveau de l'horizon. Par exemple, le champ de vue de l'image ci-dessous est de 100° car, au niveau de l'horizon, le bord gauche est à l'azimut 40° tandis que le bord droit est à l'azimut 140°.

fov;32.png

En changeant le champ de vue de l'image du ciel affichée à l'écran, il est possible de zoomer plus ou moins dans le ciel. En effet, un grand champ de vue correspond à un faible niveau de zoom, tandis qu'un petit champ de vue correspond à un fort niveau de zoom.

Pour donner une idée, le champ de vue de la vision binoculaire humaine, c-à-d l'angle correspondant à la zone visible simultanément par les deux yeux fixes d'un être humain, est d'environ 110°. Le champ de vue d'une paire de jumelles varie en fonction des modèles, mais peut aller de 3° à 15° environ.

L'utilisateur du programme Rigel aura la possibilité de changer le champ de vue de l'image dans une plage allant de 30° à 150°, qui convient bien à l'observation à l'œil nu. Ce changement se fera au moyen de la molette de la souris, et/ou du trackpad.

2.2 Champ de vue et facteur de dilatation

Imaginons un objet centré au centre de projection et dont la taille angulaire est telle que sa projection va d'un bord de l'image à l'autre. Étant donnée la définition du champ de vue, il est clair que la taille angulaire de cet objet est égale au champ de vue.

Or nous avons vu à l'étape 4 que la projection stéréographique projette un objet de taille angulaire \(\theta\) centré au centre de projection en un disque dont le diamètre \(d\) est :

\[ d = 2\tan\left(\frac{\theta}{4}\right) \]

Sachant que le diamètre \(d\) de l'objet considéré ici est égal à la largeur de l'image, que nous noterons \(w\), et que sa taille angulaire \(\theta\) est égale au champ de vue, que nous noterons \(\varphi\), on a :

\begin{align*} d &= w\\ \theta &= \varphi \end{align*}

On en déduit que :

\[ w = 2\tan\left(\frac{\varphi}{4}\right) \]

Nous avons vu à l'étape 8 que le passage du repère de la projection stéréographique au repère du canevas se fait en combinant une translation et une dilatation. Si on nomme le facteur de dilatation \(s\), alors on sait que la largeur \(w\) de l'image dans le repère de la projection stéréographique est liée à sa largeur \(W\) dans le repère du canevas par :

\[ W = s\cdot w \]

En substituant \(w\) dans l'équation ci-dessus par la partie de droite de l'équation plus haut, on obtient :

\[ W = s\cdot 2\tan\left(\frac{\varphi}{4}\right) \]

On en conclut que le facteur de dilatation \(s\) peut se calculer ainsi, étant donné le champ de vue \(\varphi\) :

\[ s = \frac{W}{2\tan\left(\frac{\varphi}{4}\right)} \]

Par exemple, pour dessiner une image de 800 unités de large avec un champ de vue de 68.4°, le facteur de dilatation à utiliser est :

\[ s = \frac{800}{2\tan\left(\frac{68.4°}{4}\right)} \approx 1300 \]

Il en découle que le champ de vue de l'image produite par le programme DrawSky de l'étape 8 est d'environ 68.4°.

2.3 Centre de projection

La projection stéréographique que nous utilisons projette le centre de projection au centre de l'image. Il paraît donc raisonnable de considérer que le centre de projection correspond à la direction dans laquelle l'observateur regarde le ciel.

Par exemple, si le centre de projection utilisé est à l'azimut 180° et à la hauteur 45°, on considère que l'observateur regarde plein sud et à mi-hauteur entre l'horizon et le zénith.

L'utilisateur du programme Rigel aura la possibilité de changer la direction de regard — donc le centre de projection — au moyen des touches du curseur (touches fléchées).

Logiquement, les touches gauche/droite changeront l'azimut de la direction de regard, par pas de 10°. La touche gauche enlèvera 10° à l'azimut, tandis que la touche droite les ajoutera. Bien entendu, cet azimut sera maintenu dans l'intervalle canonique allant de 0° (inclus) à 360° (exclus). En d'autres termes, si l'azimut actuel de la direction de regard est de 5° et que l'utilisateur presse la touche du curseur gauche, il devient 355°.

Les touches haut/bas changeront quant à elles la hauteur de la direction de regard, par pas de 5°. La touche haut ajoutera 5° à la hauteur, tandis que la touche bas les enlèvera. Cette hauteur sera limitée à l'intervalle allant de 5° (inclus) à 90° (inclus). Le regard sera donc toujours dirigé au-dessus de l'horizon, ce qui est logique car la portion du ciel située sous l'horizon est cachée par la Terre.

3 Mise en œuvre Java

Avant de commencer à mettre en œuvre cette étape, il est capital que vous compreniez bien les propriété JavaFX. Au besoin, relisez donc la totalité de la §4 des notes de cours sur JavaFX.

Sachez de plus que vous aurez souvent besoin d'utiliser certaines méthodes de la classe Bindings, en particulier createDoubleBinding et createObjectBinding. Ces méthodes, et les autres dont le nom commence par create et se termine par Binding, ont pour but de faciliter la création d'un lien (binding) dont la valeur dépend de celle d'autres liens.

Le premier argument de ces méthodes est une lambda sans arguments, qui est appelée pour déterminer la valeur du lien. Les arguments qui suivent cette lambda — il peut y en avoir un nombre quelconque — sont les liens dont dépend le lien à créer. La lambda est appelée chaque fois que la valeur d'un lien dont le nouveau dépend change, et elle doit (re)calculer la valeur du lien et la retourner.

Ces liens sont donc très similaires à des cellules d'un tableur contenant une formule. La lambda contient le code de la formule en question, tandis que les liens dépendants représentent les cellules dont la valeur est utilisée par la formule.

Le petit programme ci-dessous illustre la création d'un lien de ce type au moyen de la méthode createStringBinding. Lisez-le, essayez de l'exécuter, et n'hésitez pas à le modifier pour vous assurer de comprendre comment il fonctionne.

public final class UseCreateBinding {
  public static void main(String[] args) {
    StringProperty s = new SimpleStringProperty("Hi!");
    IntegerProperty b = new SimpleIntegerProperty(0);
    IntegerProperty e = new SimpleIntegerProperty(3);

    ObservableStringValue ss = Bindings.createStringBinding(
      () -> s.getValue().substring(b.get(), e.get()),
      s, b, e);
    ss.addListener(o -> System.out.println(ss.get()));

    System.out.println("----");
    s.set("Hello, world!");
    s.setValue("Bonjour, monde !");
    e.set(16);
    b.set(9);
    System.out.println("----");
  }
}

Si vous ne comprenez pas pourquoi s, b et e sont passés en arguments, après la lambda, à createStringBinding, essayez de les enlever l'un après l'autre, et voyez comment le comportement du programme change. Comprenez-vous pourquoi il change ainsi ?

Si vous ne comprenez pas quand la lambda passée à createStringBinding est appelée, essayez de le deviner puis modifiez son code pour lui faire afficher quelque chose à chaque appel. Vos hypothèses étaient-elles correctes ?

Attention : ne poursuivez pas avant d'avoir bien compris le fonctionnement du programme ci-dessus.

3.1 Classe ViewingParametersBean

La classe ViewingParametersBean du paquetage ….gui, publique et instanciable, est un bean JavaFX contenant les paramètres déterminant la portion du ciel visible sur l'image. Les deux propriétés de ce bean sont :

  1. le champ de vue (en degrés), nommé p.ex. fieldOfViewDeg, et
  2. les coordonnées du centre de projection, nommées p.ex. center.

Cette classe est très similaire à la classe DateTimeBean de l'étape précédente, mais possède bien entendu des propriétés différentes.

3.2 Classe ObserverLocationBean

La classe ObserverLocationBean du paquetage ….gui, publique et instanciable, est un bean JavaFX contenant la position de l'observateur, en degrés. Les trois propriétés qu'il possède sont :

  1. la longitude de la position de l'observateur, en degrés, nommée p.ex. lonDeg,
  2. la latitude de la position de l'observateur, en degrés, nommée p.ex. latDeg,
  3. les deux coordonnées précédentes, mais combinées en une instance de GeographicCoordinates, nommée p.ex. coordinates.

La dernière de ces « propriétés » est en fait un lien créé au moyen de la méthode createObjectBinding et dont la valeur est déterminée par les deux autres propriétés.

L'intérêt d'offrir ainsi la position de l'observateur sous deux formes, à savoir « déconstruite » (longitude et latitude séparée) et composite (longitude et latitude combinées dans une instance de GeographicCoordinates) est que cela facilite la construction de l'interface utilisateur, comme nous le verrons à l'étape suivante.

3.3 Classe SkyCanvasManager

La classe SkyCanvasManager du paquetage ….gui, publique et instanciable, est un gestionnaire de canevas sur lequel le ciel est dessiné. Ses principales tâches sont de :

  • créer le canevas sur lequel le ciel est dessiné,
  • créer le peintre chargé du dessin du ciel et lui faire redessiner le ciel chaque fois que cela est nécessaire,
  • changer le champ de vue lorsque l'utilisateur manipule la molette de la souris et/ou le trackpad,
  • changer le centre de projection lorsque l'utilisateur appuie sur les touches du curseur,
  • suivre les mouvements de la souris et exporter, via des propriétés, la position de son curseur dans le système de coordonnées horizontal, et l'objet céleste le plus proche de ce curseur.

Pour ce faire, son constructeur prend en arguments le catalogue d'étoiles et d'astérismes à utiliser, ainsi que les trois beans définis jusqu'à présent, et :

  • crée un certain nombre de propriétés et liens décrits plus bas,
  • installe un auditeur (listener) pour être informé des mouvements du curseur de la souris, et stocker sa position dans une propriété,
  • installe un auditeur pour détecter les clics de la souris sur le canevas et en faire alors le destinataire des événements clavier,
  • installe un auditeur pour réagir aux mouvements de la molette de la souris et/ou du trackpad et changer le champ de vue en fonction,
  • installe un auditeur pour réagir aux pressions sur les touches du curseur et changer le centre de projection en fonction,
  • installe des auditeurs pour être informé des changements des liens et propriétés ayant un impact sur le dessin du ciel, et demander dans ce cas au peintre de le redessiner.

En plus de ce constructeur, qui contient la quasi-totalité du code de la classe, SkyCanvasManager possède trois propriétés (des liens, en réalité) publiques, qui sont :

  1. l'azimut, en degrés, de la position du curseur de la souris, nommé p.ex. mouseAzDeg
  2. la hauteur, en degrés, de la position du curseur de la souris, nommé p.ex. mouseAltDeg,
  3. l'objet céleste le plus proche du curseur de la souris, s'il y en a un situé au maximum à 10 unités dans le repère du canevas, nommé p.ex. objectUnderMouse.

3.3.1 Propriétés et liens privés

Le but principal de la classe SkyCanvasManager est de créer et de connecter un certain nombre de propriétés et liens afin de garantir que l'interface réagit correctement aux actions de l'utilisateur. En plus des propriétés et liens des trois beans décrits précédemment, ainsi que certaines propriétés du canevas, SkyCanvasManager peut créer un certain nombre de propriétés et liens privés pour faciliter les choses.

Les liens et propriétés privés que nous vous conseillons de créer sont :

  • un lien contenant la projection stéréographique à utiliser, nommé p.ex. projection,
  • un lien contenant la transformation correspondant au passage du repère de la projection stéréographique au repère du canevas, nommé p.ex. planeToCanvas,
  • un lien contenant le ciel observé, nommé p.ex. observedSky,
  • une propriété contenant la position du curseur de la souris dans le repère du canevas, nommée p.ex. mousePosition,
  • un lien contenant la position du curseur de la souris dans le système de coordonnées horizontal (azimut/hauteur), nommé p.ex. mouseHorizontalPosition.

Ces liens internes, les liens externes objectUnderMouse, mouseAzDeg et mouseAltDeg, ainsi que les propriétés et liens des différents beans définis jusqu'à présent sont illustrés dans la figure ci-dessous. Les flèches reliant ces différentes propriétés et liens indiquent les dépendances. Par exemple, le lien planeToCanvas dépend des propriétés width et height du canevas, ainsi que du lien projection.

prop-dep;16.png
Figure 2 : Propriétés et liens du programme Rigel

L'étude attentive de cette figure devrait grandement faciliter l'écriture de la classe SkyCanvasManager.

3.3.2 Évènements clavier

La pression des touches du clavier peut être détectée en ajoutant un auditeur au canevas au moyen de la méthode setOnKeyPressed. Notez que les événements clavier ne sont transmis au canevas que lorsqu'il est le destinataire de ces événements — en anglais, on dit qu'il a le focus. Pour faire en sorte que ce soit le cas, il faut appeler la méthode requestFocus sur le canevas, comme l'illustre le programme d'exemple plus bas.

L'événement passé à un auditeur d'événement clavier est du type KeyEvent. La méthode getCode de cet événement permet d'obtenir un code identifiant la touche pressée. Les codes correspondants aux quatre touches du curseur sont : LEFT, RIGHT, UP et DOWN.

Notez que si une des touches du curseur est pressée, il faut absolument « consommer » l'événement généré en appelant sa méthode consume dans l'auditeur. Cela évite que JavaFX n'interprète cet événement lui-même, ce qui poserait des problèmes une fois l'interface utilisateur terminée.

3.3.3 Évènements souris

Les clics de la souris sur le canevas peuvent être détectés en lui ajoutant un auditeur au moyen de la méthode setOnMousePressed. Cet auditeur reçoit un événement de type MouseEvent, dont la méthode isPrimaryButtonDown permet de vérifier que c'est bien le bouton principal — généralement le gauche — qui est pressé. Un clic du bouton principal sur le canevas doit provoquer un appel de sa méthode requestFocus, dans le but d'en faire le destinataire des événements clavier.

Les mouvements de la souris au dessus du canevas peuvent être détectés en lui ajoutant un auditeur au moyen de la méthode setOnMouseMoved, qui reçoit également un événement de type MouseEvent. La position de la souris dans le repère du canevas s'obtient au moyen des méthodes getX et getY de l'événement.

L'utilisation de la molette de la souris ou du trackpad au dessus du canevas peut être détectée en lui ajoutant un auditeur au moyen de la méthode setOnScroll, qui reçoit un événement de type ScrollEvent. Le changement à appliquer au champ de vue, en degrés, est la plus grande valeur retournée par les méthodes getDeltaX et getDeltaY. Notez que la comparaison doit se faire sur les valeurs absolues, mais c'est bien la valeur signée qui doit être ajoutée au champ de vue, afin de permettre le zoom dans les deux sens.

3.4 Tests

Pour tester le code de cette étape, vous pouvez vous inspirer du squelette d'application JavaFX ci-dessous, qui utilise la classe SkyCanvasManager pour afficher le ciel au même moment, et depuis le même point d'observation, que le programme DrawSky de l'étape 8.

public final class UseSkyCanvasManager extends Application {
  public static void main(String[] args) { launch(args); }

  private InputStream resourceStream(String resourceName) {
    return getClass().getResourceAsStream(resourceName);
  }

  @Override
  public void start(Stage primaryStage) throws IOException {
    try (InputStream hs = resourceStream("/hygdata_v3.csv")) {
      StarCatalogue catalogue = new StarCatalogue.Builder()
	.loadFrom(hs, HygDatabaseLoader.INSTANCE)
	.build();

      ZonedDateTime when =
	ZonedDateTime.parse("2020-02-17T20:15:00+01:00");
      DateTimeBean dateTimeBean = new DateTimeBean();
      dateTimeBean.setZonedDateTime(when);

      ObserverLocationBean observerLocationBean =
	new ObserverLocationBean();
      observerLocationBean.setCoordinates(
	GeographicCoordinates.ofDeg(6.57, 46.52));

      ViewingParametersBean viewingParametersBean =
	new ViewingParametersBean();
      viewingParametersBean.setCenter(
	HorizontalCoordinates.ofDeg(180, 42));
      viewingParametersBean.setFieldOfViewDeg(70);

      SkyCanvasManager canvasManager = new SkyCanvasManager(
	catalogue,
	dateTimeBean,
	observerLocationBean,
	viewingParametersBean);

      canvasManager.objectUnderMouseProperty().addListener(
	(p, o, n) -> {if (n != null) System.out.println(n);});

      Canvas sky = canvasManager.canvas();
      BorderPane root = new BorderPane(sky);

      sky.widthProperty().bind(root.widthProperty());
      sky.heightProperty().bind(root.heightProperty());

      primaryStage.setMinWidth(800);
      primaryStage.setMinHeight(600);

      primaryStage.setY(100);

      primaryStage.setScene(new Scene(root));
      primaryStage.show();

      sky.requestFocus();
    }
  }
}

En exécutant ce programme, vous devriez pouvoir utiliser la molette de la souris ou le trackpad (probablement avec deux doigts) pour changer le champ de vue, et les touches du curseur pour changer le centre de projection, comme illustré dans la vidéo ci-dessous. De plus, le nom de l'objet le plus proche du curseur de la souris devrait s'afficher dans la console.

4 Résumé

Pour cette étape, vous devez :

  • écrire les classes ViewingParametersBean, ObserverLocationBean et SkyCanvasManager selon les instructions données plus haut,
  • 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.