Animation du ciel

Rigel – étape 9

1 Introduction

Cette étape a pour but principal d'écrire les classes qui permettront d'animer le ciel en le redessinant à intervalles réguliers, tout en faisant s'écouler le temps plus ou moins rapidement.

Un autre but de cette étape est de corriger quelques problèmes identifiés dans le code écrit jusqu'à présent.

2 Concepts

2.1 Animation du ciel

Le ciel peut être animé simplement en le redessinant périodiquement, afin de rendre perceptible le mouvement apparent des objets célestes. Étant donné que ces objets ne se déplacent que lentement, il est également intéressant d'offrir la possibilité d'accélérer l'écoulement du temps, et l'écriture du code gérant cette accélération temporelle constitue la plus grosse partie de cette étape.

Deux techniques d'accélération du temps seront proposées dans ce projet, l'une continue et l'autre discrète, et elles font l'objet des deux sections suivantes.

2.1.1 Accélération continue

L'accélération continue consiste simplement à faire en sorte que le temps simulé (simulated time) — c-à-d le temps qui détermine l'instant d'observation pour lequel le ciel est dessiné — s'écoule un certain nombre de fois plus rapidement que le temps réel (real time) — c-à-d le temps perçu par l'utilisateur du programme.

L'équation ci-dessous exprime la relation existant entre le temps réel et le temps simulé lorsque celui-ci est accéléré de manière continue :

\[ T = T_0 + \alpha\cdot (t - t_0) \]

où \(T_0\) est le temps simulé initial (au début de l'animation), \(T\) est le temps simulé actuel, \(t_0\) est le temps réel initial, \(t\) est le temps réel actuel, et \(\alpha\) est le facteur d'accélération du temps.

Par exemple, admettons que le 20 avril 2020 à 13h15, une astronome amateur désire voir comment évoluera le ciel le soir même à partir de 21h00, en l'animant à 300 fois la vitesse réelle. On a donc :

\begin{align*} T_0 &= \text{21h 00m 00s}\\ t_0 &= \text{13h 15m 00s}\\ \alpha &= 300 \end{align*}

On peut en conclure p.ex. qu'après 2.34 secondes d'animation, à \(t = \text{13h 15m 02.34s}\), le temps simulé sera :

\begin{align*} T &= \text{21h 00m 00s} + 300\cdot(\text{13h 15m 02.34s} - \text{13h 15m 00s})\\ &= \text{21h 00m 00s} + 300\cdot 2.34\text{s}\\ &= \text{21h 11m 42s} \end{align*}

2.1.2 Accélération discrète

L'accélération discrète consiste à faire s'écouler le temps simulé par pas (steps) discrets. L'équation qui lie le temps réel et le temps simulé est dans ce cas :

\[ T = T_0 + \left\lfloor \nu\cdot (t - t_0)\right\rfloor\times S \]

où \(\nu\) est la fréquence d'avancement du temps simulé, c-à-d le nombre de pas de temps simulé qui s'écoulent par unité de temps réel, \(S\) est le pas discret de temps simulé et les autres variables ont la même signification que précédemment.

Notez que l'utilisation de l'arrondi par défaut (\(\lfloor\cdot\rfloor\)) dans cette équation implique que la durée ajoutée au temps simulé initial \(T_0\) est toujours un multiple du pas \(S\). Dès lors, le temps simulé avance par à-coups, ce qui explique que cette accélération soit dite discrète.

Pour illustrer l'accélération discrète, admettons que notre astronome amateur désire cette fois voir comment les planètes se déplacent dans le ciel, sur fond d'étoiles fixe. Pour cela, elle décide de faire avancer l'animation par pas discrets dont la durée est celle d'un jour sidéral, soit 23h 56m 04s — pour mémoire, après un jour sidéral les étoiles se retrouvent à la même position dans le ciel. Elle décide de plus de faire avancer l'animation 10 fois par secondes, ce qui implique que 10 jours sidéraux de temps simulé s'écouleront pour chaque seconde de temps réel. On a donc :

\begin{align*} T_0 &= \text{21h 00m 00s}\\ t_0 &= \text{13h 15m 00s}\\ \nu &= 10\,\text{s}^{-1}\\ S &= \text{23h 56m 04s} \end{align*}

On peut en conclure qu'après 2.34 secondes d'animation, à \(t = \text{13h 15m 02.34s}\), le temps simulé sera :

\begin{align*} T &= \text{21h 00m 00s} + \left\lfloor 10\cdot(\text{13h 15m 02.34s} - \text{13h 15m 00s})\right\rfloor\times\text{23h 56m 04s}\\ &= \text{21h 00m 00s} + \left\lfloor 10\cdot 2.34\right\rfloor\times\text{23h 56m 04s}\\ &= \text{21h 00m 00s} + 23\times\text{23h 56m 04s}\\ &= \text{571h 29m 32s}\\ &= 23\times 24\text{h} + \text{19h 29m 32s} \end{align*}

Le fait que cette valeur soit supérieure à 24h signifie que la date simulée n'est plus le 20 avril 2020, mais bien le 13 mai 2020 (23 jours après), et l'heure simulée est 19h 29m 32s.

3 Mise en œuvre Java

3.1 Correction d'erreurs

Avant de mettre en œuvre cette étape, il convient de corriger quelques erreurs dans les étapes 1 à 6, qui sont décrites ci-dessous.

3.1.1 Conversion d'angles en notation sexagésimale

Lorsqu'un angle négatif est spécifié en notation sexagésimale (degrés, minutes et secondes), le signe négatif placé en tête s'applique à la totalité des composantes. Par exemple, l'angle suivant :

\(-15°\,30^\prime\,00^{\prime\prime}\)

correspond à l'angle noté –15.5° en notation décimale.

Malheureusement, la méthode ofDMS de la classe Angle, écrite durant l'étape 1, ne se comporte pas correctement avec un tel angle, et si on l'appelle avec les arguments correspondants à l'angle ci-dessus, à savoir :

Angle.ofDMS(-15, 30, 0)

elle retourne l'angle correspondant à –14.5°, ce qui est faux.

Pour corriger ce problème, nous vous proposons d'interdire purement et simplement les valeurs négatives comme premier argument de la méthode ofDMS, en lui ajoutant une précondition.

Cette solution ne pose aucun problème dans le projet, d'une part car aucun angle négatif n'est créé au moyen de ofDMS, et d'autre part car un tel angle peut toujours être créé facilement en plaçant le signe de négation devant la méthode, ainsi :

- Angle.ofDMS(15, 30, 0)

ce qui a l'avantage supplémentaire d'être plus logique.

Notez que cette modification fait échouer le test ofDMSWorksOnKnownValues, qui est malheureusement incorrect. Il faut donc supprimer le quatrième appel à assertEquals qu'il contient, qui appelle ofDMS avec un premier argument négatif.

3.1.2 Projection stéréographique inverse

Les formules d'inversion de la projection stéréographique données à la §2.1.2 de l'étape 4 ne fonctionnent pas pour l'origine, c-à-d le point de coordonnées (0, 0). Dans ce cas, elle se simplifient en :

\begin{align*} \lambda &= \arctan\left[\frac{0}{0}\right] + \lambda_0\\[0.5em] \phi &= \arcsin\left[\sin\phi_1 + \frac{0}{0}\right] \end{align*}

et leur valeur est indéfinie en raison des deux divisions par zéro. Pour corriger ce problème, il faut traiter de manière particulière le cas de l'origine, dont l'inverse est le centre de projection, en définissant les équations d'inversion ainsi :

\begin{align*} \lambda &= \begin{cases} \lambda_0 & \text{si }(x,y) = (0,0)\\ \ldots & \text{sinon} \end{cases}\\[0.5em] \phi &= \begin{cases} \phi_1 & \text{si }(x,y) = (0,0)\\ \ldots & \text{sinon} \end{cases} \end{align*}

où les ellipses (…) représentent les formules données à l'étape 4.

3.1.3 Données d'Uranus

La valeur de la longitude à J2010 (\(\varepsilon\)) de la planète Uranus donnée à l'étape 5, qui provient du livre de référence, est incorrecte dans ce dernier, comme expliqué dans son errata. Au lieu des 271.063148° donnés, elle devrait valoir 356.135400°.

3.2 Interface TimeAccelerator

L'interface TimeAccelerator du paquetage ….gui, publique, représente un « accélérateur de temps », c'est-à-dire une fonction permettant de calculer le temps simulé —  et généralement accéléré — en fonction du temps réel.

Cette interface est une interface fonctionnelle, et peut donc être annotée au moyen de @FunctionalInterface.

L'unique méthode abstraite de cette interface, nommée p.ex. adjust, prend en arguments :

  • le temps simulé initial \(T_0\), représenté par une valeur de type ZonedDateTime,
  • le temps réel écoulé depuis le début de l'animation — soit la valeur de l'expression \((t - t_0)\) plus haut — représenté par une valeur de type long exprimée en nanosecondes (!).

Au moyen de ces arguments, elle calcule le temps simulé \(T\) et le retourne sous la forme d'une nouvelle instance de ZonedDateTime.

Le temps réel écoulé est exprimé en nanosecondes car c'est la représentation utilisée par JavaFX, comme nous le verrons plus bas.

En plus de la méthode abstraite décrite ci-dessus, l'interface TimeAccelerator offre deux méthodes statiques, permettant chacune de créer l'un des deux types d'accélérateurs décrits dans l'introduction :

  • continuous retourne un accélérateur continu en fonction du facteur d'accélération \(\alpha\) (entier),
  • discrete retourne un accélérateur discret en fonction de la fréquence d'avancement \(\nu\) (entière) et du pas \(S\).

Ces deux méthodes se définissent très simplement et élégamment au moyen de lambdas, et le pas d'un accélérateur discret est très naturellement représenté par une instance de la classe Duration de la bibliothèque Java.

3.3 Type énuméré NamedTimeAccelerator

Le type énuméré NamedTimeAccelerator du paquetage ….gui, publique, représente un accélérateur de temps nommé, c'est-à-dire une paire (nom, accélérateur).

Les membres de ce type sont les accélérateurs qui seront disponibles à l'utilisateur du programme une fois celui-ci terminé, et les noms seront ceux affichés à l'écran.

La table ci-dessous présente les noms et caractéristiques des six membres disponibles. Notez que le nom donné dans la première colonne est celui qui sera visible à l'écran, et il est stocké dans un attribut de type String du membre, tandis que le nom donné dans la dernière colonne est celui que nous vous suggérons d'utiliser pour nommer le membre de l'énumération Java.

Nom Type Nom du membre
Continu, \(\alpha = 1\) TIMES_1
30× Continu, \(\alpha = 30\) TIMES_30
300× Continu, \(\alpha = 300\) TIMES_300
3000× Continu, \(\alpha = 3000\) TIMES_3000
jour Discret, \(S=\text{24h}, \nu=60\,\text{s}^{-1}\) DAY
jour sidéral Discret, \(S=\text{23h 56m 04s}, \nu=60\,\text{s}^{-1}\) SIDEREAL_DAY

Le type énuméré NamedTimeAccelerator offre trois méthodes, qui sont :

  • getName, qui retourne le nom de la paire,
  • getAccelerator, qui retourne l'accélérateur de la paire,
  • toString, qui redéfinit la méthode de Object afin qu'elle retourne le nom de la paire, tout comme getName.

Notez qu'il est très important que le nom des deux méthodes d'accès commence par get car cette convention de nommage est utilisée par JavaFX. En nommant ces méthodes d'accès autrement — par exemple sans le get initial, comme nous l'avons fait jusqu'à présent —, vous rendriez le type énuméré NamedTimeAccelerator inutilisable dans une interface JavaFX.

3.4 Classe DateTimeBean

Cette section et la suivante utilisent des concepts qui, au moment de la publication de cette étape, n'auront pas encore été vus au cours. Avant de continuer, il est donc fortement recommandé de lire la §4.1 et la §4.3 des notes de cours sur les interfaces graphiques JavaFX, consacrées aux propriétés et aux beans JavaFX.

La classe DateTimeBean du paquetage ….gui, publique et finale, est un bean JavaFX contenant l'instant d'observation, c-à-d le triplet (date, heure, fuseau horaire) d'observation. Il s'agit d'une classe très simple possédant les propriétés JavaFX suivantes :

  • date, la date d'observation, de type LocalDate,
  • time, l'heure d'observation, de type LocalTime,
  • zone, le fuseau horaire d'observation, de type ZoneId.

Toutes ces propriétés ont une valeur initiale égale à null. Attention, cela ne signifie pas que les propriétés elles-mêmes sont nulles, mais que leur contenu l'est.

Comme expliqué à la §4.3 des notes de cours sur JavaFX, à chacune de ces propriétés correspondent trois méthodes dans la classe DateTimeBean :

  1. une méthode donnant accès à la propriété elle-même,
  2. une méthode donnant accès au contenu de la propriété,
  3. une méthode permettant de modifier le contenu de la propriété.

Par exemple, pour la propriété date, ces méthodes sont, dans l'ordre :

  1. ObjectProperty<LocalDate> dateProperty(),
  2. LocalDate getDate(), et
  3. void setDate(LocalDate date).

En plus des méthodes liées aux propriétés, la classe DateTimeBean en fournit deux qui permettent d'obtenir ou de modifier l'instant d'observation sous la forme d'une valeur de type ZonedDateTime. Ces méthodes pourraient être :

  1. getZonedDateTime, qui retourne l'instant d'observation sous la forme d'une valeur de type ZonedDateTime,
  2. setZonedDateTime, qui modifie l'instant d'observation pour qu'il soit égal à la valeur de type ZonedDateTime qu'on lui passe en argument.

Ces méthodes s'expriment trivialement en termes de celles travaillant sur le contenu des propriétés date, time et zone.

Il faut bien comprendre que DateTimeBean contient exactement les mêmes composantes qu'une instance de ZonedDateTime, à savoir une date, une heure et un fuseau horaire. La différence entre ces deux classes est que, dans DateTimeBean, ces composantes sont des propriétés JavaFX observables — et donc pas immuables. On peut donc voir DateTimeBean comme une version modifiable et observable de ZonedDateTime.

3.5 Classe TimeAnimator

La classe TimeAnimator du paquetage ….gui, publique et finale, représente un « animateur de temps ». Son but est de modifier périodiquement, via un accélérateur de temps, l'instant d'observation stocké dans une instance de DateTimeBean, dans le but d'animer ainsi (indirectement) le ciel. L'instance de DateTimeBean dont le temps d'observation doit être modifié de la sorte est passée en argument au constructeur de TimeAnimator, tandis que l'accélérateur lui est transmis par la propriété accelerator décrite plus bas.

La classe TimeAnimator hérite de la classe AnimationTimer de JavaFX. AnimationTimer est une classe (abstraite) très simple, représentant un minuteur d'animation dont le but est de faire évoluer une (ou plusieurs) animation(s) d'une application JavaFX. Pour ce faire, dès qu'un minuteur est démarré au moyen de sa méthode start, sa méthode handle est automatiquement appelée environ 60 fois par secondes afin de faire progresser l'animation. Pour arrêter le minuteur, il suffit d'appeler sa méthode stop.

La méthode handle reçoit en argument une valeur représentant le nombre de nanosecondes écoulées depuis un instant de départ non spécifié. Comme l'origine de ce temps en nanosecondes n'est pas spécifiée, sa valeur absolue n'a pas vraiment de signification, mais il est par contre possible d'utiliser la différence des valeurs passées à des appels successifs à handle pour mesurer le temps écoulé. Il est même assez simple de mesurer le temps écoulé depuis le début d'une animation, en s'assurant que lorsque la méthode handle est appelée pour la première fois après le démarrage du minuteur, elle stocke le nombre de nanosecondes qui lui est passé dans un attribut du minuteur.

En plus de la définition de la méthode handle, la classe TimeAnimator possède les deux propriétés suivantes :

  • accelerator, qui contient l'accélérateur de temps, de type TimeAccelerator, valant initialement null,
  • running, qui contient l'état de l'animateur, de type bool — vrai si l'animation est en train de s'exécuter, c-à-d si le temps simulé est périodiquement mis à jour, faux sinon.

La propriété running doit bien entendu être accessible en lecture seulement, donc impossible à modifier depuis l'extérieur.

3.5.1 Conseils de programmation

La propriété running doit être modifiable depuis l'intérieur de la classe TimeAnimator, mais pas depuis l'extérieur. Le moyen le plus simple de garantir cela est de la représenter par une instance de SimpleBooleanProperty mais de la fournir à l'extérieur avec le type ReadOnlyBooleanProperty. Même si cela signifie qu'en pratique un simple transtypage permettrait de modifier la propriété depuis l'extérieur, cela n'est pas important car le but ici est de se protéger contre des erreurs de programmation non intentionnelles, et pas contre un programmeur malveillant.

Un moyen simple de savoir quand mettre à jour la propriété running consiste à redéfinir les méthodes start et stop de AnimationTimer. La redéfinition de start peut également faire le nécessaire pour que handle soit à même de savoir quand elle est appelée pour la première fois après le démarrage du minuteur, et ainsi mémoriser le temps du début de l'animation.

3.6 Tests

Pour tester vos classes, vous pouvez vous inspirer du petit programme ci-dessous, une application JavaFX qui utilise un animateur de temps pour afficher la manière dont le temps simulé évolue lorsqu'on accélère le temps réel d'un facteur 3000.

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

  @Override
  public void start(Stage primaryStage) {
    ZonedDateTime simulatedStart =
      ZonedDateTime.parse("2020-06-01T23:55:00+01:00");
    TimeAccelerator accelerator =
      NamedTimeAccelerator.TIMES_3000.getAccelerator();

    DateTimeBean dateTimeB = new DateTimeBean();
    dateTimeB.setZonedDateTime(simulatedStart);

    TimeAnimator timeAnimator = new TimeAnimator(dateTimeB);
    timeAnimator.setAccelerator(accelerator);

    dateTimeB.dateProperty().addListener((p, o, n) -> {
	System.out.printf(" Nouvelle date : %s%n", n);
	Platform.exit();
      });
    dateTimeB.timeProperty().addListener((p, o, n) -> {
	System.out.printf("Nouvelle heure : %s%n", n);
      });
    timeAnimator.start();
  }
}

En l'exécutant vous devriez voir des lignes similaires à celles ci-dessous s'afficher sur votre console :

Nouvelle heure : 23:55:46.073088
Nouvelle heure : 23:56:36.153303
Nouvelle heure : 23:57:26.100501
Nouvelle heure : 23:58:16.676541
Nouvelle heure : 23:59:09.397014
Nouvelle heure : 23:59:56.754609
 Nouvelle date : 2020-06-02
Nouvelle heure : 00:00:46.465743

Notez que les valeurs exactes changent d'une exécution à l'autre. Toutefois, les heures successives devraient être séparées les unes des autres d'environ 50 secondes, car le temps simulé s'écoule 3000 fois plus vite que le temps réel, et JavaFX fait avancer les animations à 60 images par secondes environ. On a donc :

\[ 3000 \times \frac{1}{60}\,\mathrm{s} = 50\,\mathrm{s} \]

Suivant la manière dont vous avez écrit votre classe DateTimeBean, il est également possible que les deux dernières lignes soient inversées, ce qui ne pose aucun problème.

Notez que si vous remplacez l'accélérateur continu utilisé ci-dessus par un accélérateur discret, alors la durée séparant les couples date/heure successifs devrait être exactement égale au pas de l'accélérateur.

4 Résumé

Pour cette étape, vous devez :

  • corriger les erreurs décrites plus haut dans les classes Angle, StereographicProjection et dans le type énuméré PlanetModel,
  • écrire les classes, types énumérés et interfaces TimeAccelerator, NamedTimeAccelerator, DateTimeBean et TimeAnimator 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.