Méthodes mathématiques
Etape 1

1 Préliminaires

Avant de commencer à travailler à cette première étape du projet, lisez les guides Travailler en groupe, Synchroniser son travail et Configurer Eclipse. Ils vous donneront des informations vous permettant de bien débuter ce projet.

2 Introduction

Cette première étape a pour but d'écrire un certain nombre de méthodes utilitaires qui seront nécessaires dans la suite du projet. Ces différentes méthodes n'ont pas vraiment de lien entre elles, raison pour laquelle cette étape est assez hétéroclite.

Néanmoins, la quasi-totalité du code à écrire est de nature mathématique. De plus, une partie non négligeable est liée à la géométrie sphérique, qui joue un rôle important dans ce projet étant donné la forme de la Terre.

Comme toutes les descriptions d'étapes de ce projet, celle-ci commence par une introduction aux concepts nécessaires à sa réalisation, avant de présenter leur mise en œuvre en Java.

2.1 Géométrie sphérique

La forme de la Terre est très irrégulière et ne peut être représentée exactement au moyen d'un objet géométrique simple. Toutefois, pour la plupart des applications, il est possible de l'approximer soit par un ellipsoïde de révolution, soit par une sphère.

L'approximation par un ellipsoïde est la plus précise, car la Terre est légèrement aplatie aux pôles. Pour cette raison, une telle approximation est utilisée lorsqu'une précision importante est requise. Par exemple, les coordonnées obtenues au moyen d'un appareil GPS (longitude et latitude) font référence à un ellipsoïde.

Pour les applications nécessitant une précision moindre, il est également possible d'approximer la forme de la Terre par une sphère. En effet, son aplatissement aux pôles est faible — environ 0.3% — et elle est donc quasi sphérique.

Pour le dessin de panoramas, l'approximation sphérique suffit largement et a l'avantage de simplifier les calculs par rapport à l'approximation ellipsoïdale. Nous l'utiliserons donc ici, en admettant que la Terre est une sphère parfaite de rayon R = 6371 km.

2.1.1 Grands cercles

Dans le plan, le plus court chemin d'un point à un autre est un segment de la droite les reliant. A la surface d'une sphère, par contre, le plus court chemin entre deux points est un arc du cercle passant par les deux points et possédant le même centre que la sphère. Un tel cercle se nomme grand cercle et son rayon est bien entendu égal à celui de la sphère.

Comme tout arc de cercle, un arc de grand cercle peut être représenté de deux manières différentes :

  1. par sa longueur \(l\),
  2. par son angle \(\alpha\).

Bien entendu, pour une sphère de rayon unitaire, ces deux valeurs sont égales si l'angle est exprimé en radians. Pour une sphère de rayon \(r\), la relation entre ces deux valeurs est :

\[ l = \alpha\cdot r \]

Par exemple, la distance séparant les villes de Lausanne et Moscou à la surface de la Terre est d'environ 2370 km, ce qui correspond à un angle de :

\[ \alpha = \frac{2\,370}{6\,371} \approx 0.372\,\mathrm{rad} \approx 21.3° \]

2.1.2 Demi sinus verse

La formule permettant de calculer la longueur de l'arc de grand cercle reliant deux points à la surface d'une sphère sera présentée à l'étape suivante. Comme nous le verrons, elle fait usage d'une fonction trigonométrique nommée haversine en anglais, définie ainsi :

\[ \newcommand{\haversin}{\mathop{\rm haversin}\nolimits} \haversin x = \left[\sin\left(\frac{x}{2}\right)\right]^2 \]

Le terme haversine vient probablement du fait que cette fonction est égale à la moitié (half) d'une autre fonction trigonométrique nommée versine (sinus verse en français).

2.2 Système de coordonnées horizontales

Un observateur placé à la surface de la Terre et désirant décrire la position des objets qu'il observe autour de lui — montagnes au loin, étoiles dans le ciel, etc. — peut le faire au moyen d'un système de coordonnées horizontales. Dans un tel système, la position d'un objet est décrite par deux angles :

  • l'azimut1, qui est l'angle horizontal entre un plan vertical de référence (p.ex. celui contenant le nord et le sud géographiques) et le plan vertical contenant l'objet,
  • l'angle d'élévation, ou simplement l'élévation, aussi appelée hauteur, qui est l'angle vertical entre le plan horizontal et l'objet désigné.

Ces angles sont illustrés dans l'image ci-dessous. L'observateur se trouve au centre de l'hémisphère, indiqué par O, et le plan de son horizon est grisé. La position du point observé (en rouge) est décrite par son azimut a et son élévation h.

Sorry, your browser does not support SVG.

Figure 1 : Système de coordonnées horizontales

L'azimut est compris entre 0° (inclus) et 360° (exclus), et il croît dans le sens horaire, ce qui est contraire à la convention mathématique. L'image ci-dessous illustre cette convention et montre les azimuts correspondants aux quatre points cardinaux (nord, sud, est et ouest) ainsi qu'aux quatre points intercardinaux principaux (nord-est, nord-ouest, sud-est et sud-ouest). La zone blanche est l'octant entourant le nord et tout azimut correspondant à cette zone sera, dans certaines situations, considéré comme équivalent au nord. Des zones de même tailles, non dessinées, entourent chacun des points cardinaux et intercardinaux.

Sorry, your browser does not support SVG.

Figure 2 : Azimut des points cardinaux et intercardinaux

Notez qu'il serait tout à fait possible de considérer que les valeurs hors de l'intervalle [0°; 360°[ constituent également des azimuts valides, interprétés modulo 360. Par exemple, -45° serait équivalent à 315°, et 370° serait équivalent à 10°. Néanmoins, il est généralement plus simple de manipuler ce que nous nommerons des azimuts canoniques, c-à-d compris dans l'intervalle [0°; 360°[.

L'angle d'élévation est compris entre -90° et 90° (inclus), et 0° correspond à l'horizon, 90° aux points situés verticalement au-dessus de l'observateur (zénith) et -90° aux points situés verticalement au-dessous de lui (nadir).

2.3 Fonctions mathématiques

Différentes fonctions mathématiques seront nécessaires à ce projet et méritent une brève introduction.

2.3.1 Reste de la division

Aussi surprenant que cela puisse paraître, il existe plusieurs définitions raisonnables de la division entière et de son reste. Celle qui est fournie par Java, via les opérateurs / et %, est appelée division tronquée. Comme son nom l'indique, elle est définie par troncation du résultat de la division réelle, c-à-d suppression des décimales. Cette forme de la division entière et de son reste peuvent se définir ainsi :

\begin{align*} \newcommand{\bdiv}{\mathop{\rm div}\nolimits} \newcommand{\trunc}{\mathop{\rm trunc}\nolimits} n \bdiv_{\rm T} d &= \trunc\left(\frac{n}{d}\right)\\ n \bmod_{\rm T} d &= n - d\cdot(n \bdiv_{\rm T} d) \end{align*}

où \(\bdiv_{\rm T}\) représente la division entière tronquée d'un numérateur \(n\) par un dénominateur \(d\), \(\bmod_{\rm T}\>\) son reste et \(\trunc\) la troncation des décimales.

Une autre définition de la division entière, nommée division entière par défaut (floored division en anglais), peut être définie de manière similaire :

\begin{align*} n \bdiv_{\rm F} d &= \left\lfloor\frac{n}{d}\right\rfloor\\ n \bmod_{\rm F} d &= n - d\cdot(n \bdiv_{\rm F} d) \end{align*}

où l'opérateur \(\lfloor\cdot\rfloor\) est l'arrondi par défaut (floor), qui retourne le plus grand entier plus petit ou égal à son argument.

Lorsque le numérateur \(n\) et le dénominateur \(d\) sont positifs, ces deux paires de fonctions produisent le même résultat. Par contre, lorsque le numérateur est négatif et le dénominateur positif, la division entière tronquée produit un reste négatif, tandis que la division entière par défaut produit un reste positif. Cela s'illustre facilement au moyen d'un exemple, la division de -11 par 4. Avec la division tronquée, on obtient :

\begin{align*} -11 \bdiv_{\rm T} 4 &= \trunc\left(\frac{-11}{4}\right) = \trunc(-2.75) = -2\\ -11 \bmod_{\rm T} 4 &= -11 - 4\cdot (-2) = -11 + 8 = -3\\ \end{align*}

alors qu'avec la division par défaut, on obtient :

\begin{align*} -11 \bdiv_{\rm F} 4 &= \left\lfloor\frac{-11}{4}\right\rfloor = \lfloor -2.75\rfloor = -3\\ -11 \bmod_{\rm F} 4 &= -11 - 4\cdot (-3) = -11 + 12 = 1 \end{align*}

Ces deux formes de division entière peuvent bien entendu s'appliquer également à des numérateurs et dénominateurs réels plutôt qu'entiers, et Java permet d'ailleurs l'application de l'opérateur % à des valeurs à virgule flottante (float ou double).

En pratique, il s'avère que la division entière par défaut est souvent préférable à la division entière tronquée. Par exemple, lorsqu'on désire ramener une valeur \(\alpha\) quelconque représentant un angle en degrés à une valeur \(\alpha^\prime\) représentant le même angle mais dans l'intervalle \([0; 360[\), on peut le faire très facilement au moyen du reste de la division entière par défaut :

\[ \alpha^\prime = \alpha\bmod_{\rm F} 360 \]

alors que la formule est plus complexe si on utilise la division tronquée, car elle produit un reste négatif si l'angle \(\alpha\) l'est également.

2.3.2 Différence angulaire

Nous nommerons différence angulaire la différence signée entre deux angles, comprise dans l'intervalle \([-\pi; \pi[\). Par exemple, la différence d'angle entre l'aiguille des heures et celles des minutes d'une horloge indiquant 9h00 est de \(-\tfrac{\pi}{2}\). Cet angle est négatif car il faut se déplacer dans le sens horaire — donc mathématiquement négatif — pour aller de l'aiguille des heures à celles des minutes.

Cette différence angulaire, notée \(\Delta\), se calcule ainsi :

\[ \Delta(\theta_1, \theta_2) = \left[(\theta_2 - \theta_1 + \pi)\bmod_{\rm F} 2\pi\right] - \pi \]

Par exemple, pour une horloge indiquant 9h00, l'angle fait par l'aiguille des heures est \(\theta_1 = \tfrac{\pi}{2}\), tandis que celui fait par l'aiguille des minutes est \(\theta_2 = 0\). Dès lors, la différence angulaire entre les deux vaut :

\begin{align*} \Delta\left(\frac{\pi}{2}, 0\right) &= \left[\left(0 - \frac{\pi}{2} + \pi\right)\bmod_{\rm F} 2\pi\right] - \pi\\ &= \frac{\pi}{2} - \pi\\ &= -\frac{\pi}{2} \end{align*}

La figure ci-dessous donne trois exemples d'angles dont la différence est cette fois toujours \(+\tfrac{\pi}{2}\).

Sorry, your browser does not support SVG.

Figure 3 : Paires d'angles dont la différence \(\Delta(\theta_1, \theta_2)\) vaut toujours \(\tfrac{\pi}{2}\)

2.3.3 Interpolation linéaire

En informatique, il arrive souvent que l'on ne connaisse la valeur d'une fonction continue \(f: \Bbb{R}\rightarrow\Bbb{R}\) qu'en un certain nombre de points espacés régulièrement. Cette situation est illustrée à gauche de la figure 4 ci-dessous.

Dans un tel cas, si l'on désire évaluer la fonction en un point autre que ceux connus, il faut utiliser une technique pour l'interpoler, c-à-d combler les trous entre les valeurs connues.

L'une des techniques d'interpolation les plus simples consiste à joindre les valeurs connues au moyen de segments de droites, comme illustré à droite de la figure 4. Cette technique s'appelle interpolation linéaire (linear interpolation, souvent abrégé lerp en anglais).

Sorry, your browser does not support SVG.

Figure 4 : Une fonction discrète et son interpolation linéaire

L'interpolation linéaire a l'avantage d'être très simple et peu coûteuse à calculer, mais elle possède également plusieurs inconvénients. Par exemple, la fonction interpolée n'est pas dérivable partout. Malgré cela, l'interpolation linéaire convient dans certaines situations et nous l'utiliserons à plusieurs reprises dans ce projet.

2.3.4 Interpolation bilinéaire

L'interpolation linéaire s'applique aux fonctions à une variable, mais il est possible de la généraliser aux fonctions à deux variables, que l'on rencontre aussi souvent en informatique.

Cette généralisation peut être illustrée au moyen d'un exemple, représenté graphiquement à la figure 5 ci-dessous. On fait ici l'hypothèse que l'on désire obtenir une valeur interpolée pour le point \((x,y)\) en connaissant la valeur de la fonction en quatre points voisins. Pour ce faire, on procède à trois interpolations linéaires successives :

  1. une première entre les valeurs \(f(x_0, y_0)\) et \(f(x_1, y_0)\) pour obtenir la valeur interpolée nommée \(z_1\),
  2. une seconde entre les valeurs \(f(x_0, y_1)\) et \(f(x_1, y_1)\) pour obtenir la valeur interpolée nommée \(z_2\),
  3. une dernière entre \(z_1\) et \(z_2\) pour obtenir la valeur interpolée finale, \(z\).

Cette technique s'appelle interpolation bilinéaire (bilinear interpolation, souvent abrégé bilerp en anglais).

Sorry, your browser does not support SVG.

Figure 5 : Interpolation bilinéaire – vue isométrique et de dessus

Notez que le terme bilinéaire pourrait faire penser qu'une telle interpolation combine deux interpolations linéaires, mais comme l'exemple ci-dessus l'illustre, elle en combine en réalité trois. Le préfixe bi fait référence au fait qu'une technique d'interpolation linéaire est utilisée dans chacune des deux dimensions.

2.3.5 Zéros d'une fonction

Comme l'explique le document d'introduction, le dessin d'un panorama se fait en déterminant l'intersection de rayons partant de l'observateur avec le sol. Rechercher un point d'intersection entre un rayon et le sol équivaut à rechercher un zéro de la fonction donnant la différence d'altitude entre le rayon et le sol. En effet, lorsque cette différence d'altitude est nulle, le rayon touche le sol.

Dès lors, il est nécessaire de savoir comment rechercher les zéros d'une fonction continue quelconque. Pour différentes raisons, ce problème est compliqué en général, mais une technique simple appelée méthode de dichotomie convient bien pour ce projet.

L'idée de la méthode de dichotomie est la suivante : admettons que l'on recherche un zéro d'une fonction \(f: \Bbb{R}\rightarrow\Bbb{R}\) continue et que l'on connaisse deux valeurs \(x_1\) et \(x_2\) telles que \(f(x_1)\) et \(f(x_2)\) soient de signe différent. Dans ce cas, on sait qu'au moins un zéro se trouve entre \(x_1\) et \(x_2\). On calcule alors \(x_m = (x_1 + x_2) / 2\) et on distingue trois cas :

  1. si \(f(x_m) = 0\) alors \(x_m\) est un zéro de \(f\),
  2. sinon, si \(f(x_m)\) n'a pas le même signe que \(f(x_1)\), alors la recherche se poursuit dans l'intervalle \([x_1; x_m]\), qui contient au moins un zéro et dont la largeur est la moitié de celle de l'intervalle original \([x_1; x_2]\),
  3. sinon, la recherche se poursuit dans l'intervalle \([x_m; x_2]\).

A noter qu'il est aussi possible — et généralement conseillé — d'arrêter la recherche lorsque l'intervalle a une taille inférieure à la précision désirée.

La méthode de dichotomie converge relativement lentement par rapport à d'autres méthodes, mais elle a l'avantage de bien se comporter dans tous les cas qui nous intéressent, raison pour laquelle nous l'utiliserons. Il faut aussi noter qu'elle suppose que l'on connaît deux valeurs \(x_1\) et \(x_2\) telles que le signe de \(f(x_1)\) et \(f(x_2)\) diffère. Il reste donc à savoir comment déterminer ces deux valeurs.

Pour ce projet, une technique simple et raisonnablement rapide consiste à tester tous les points espacés d'une certaine distance et de s'arrêter à la première paire satisfaisant la condition ci-dessus. Par exemple, on peut imaginer utiliser une distance de 2 unités et tester si \(f(0)\) et \(f(2)\) sont de signe différent. Si oui, le premier intervalle contenant un zéro a été trouvé, sinon on poursuit la recherche avec \(f(2)\) et \(f(4)\), et ainsi de suite.

Bien entendu, il est possible que cette technique ne détecte pas un zéro si un intervalle de recherche en contient un nombre pair. Pour le dessin de panoramas, cela implique qu'il est possible qu'un rayon traverse l'arête d'une montagne si celle-ci est moins large que l'intervalle de recherche utilisé. Toutefois, en utilisant un intervalle raisonnablement petit, p.ex. 60 m, on peut avoir la quasi-certitude que l'impact de ce problème sera négligeable.

3 Mise en œuvre Java

Toutes les classes et interfaces de ce projet appartiendront au paquetage ch.epfl.alpano ou à l'un de ses sous-paquetages.

3.1 Classification des méthodes

La plupart des méthodes de cette étape travaillent sur de simples nombres en virgule flottante de type double. Idéalement, de telles méthodes devraient pouvoir être définies hors de toute classe ou interface, mais Java ne permet pas cela. Dès lors, il faut décider dans quelle classe ou interface les placer.

Une solution souvent utilisée, p.ex. pour la classe Math du paquetage java.lang, consiste à définir de telles méthodes comme des méthodes statiques d'une classe définie uniquement dans ce but. Généralement, le constructeur d'une telle classe est rendu privé, ce qui interdit la création d'instances.

Depuis Java 8, une solution alternative consiste à déclarer ces méthodes comme méthodes statiques d'une interface, et c'est ce que nous avons choisi de faire. Un petit avantage de cette solution est qu'il n'est pas nécessaire de définir un constructeur privé, car une interface n'en a pas. Par contre, elle interdit la définition de méthodes auxiliaires privées, ce qui peut parfois être utile.

Dans tous les cas, il est important de comprendre que ce genre de classes ou interfaces n'ont pas pour but de jouer leur rôle « normal » dans un programme Java : une classe comme java.lang.Math n'a pas pour but d'être instanciée ou de servir de classe mère, et les interfaces définies ci-dessous n'ont pas pour but d'être implémentée par une classe quelconque. Elles ne servent que de conteneur à des méthodes statiques qui devraient être déclarées de manière indépendante si Java le permettait.

A l'utilisation, il est fortement conseillé d'importer statiquement les attributs et méthodes de ces classes ou interfaces, comme dans l'exemple ci-dessous :

import static java.lang.Math.PI;
import static java.lang.Math.sin;

public final class Main {
  public static void main(String[] args) {
    System.out.println("sin(π) = " + sin(PI));
  }
}

3.2 Interface Preconditions

Fréquemment, les méthodes d'un programme s'attendent à ce que leurs arguments satisfassent certaines conditions. Par exemple, une méthode prenant un angle en argument peut s'attendre à ce que celui-ci se trouve dans l'intervalle \([0; 2\pi[\). De telles conditions sont parfois appelées préconditions car elles doivent être satisfaites avant l'appel d'une méthode, par l'appelant.

En Java, la convention veut que chaque méthode vérifie ses préconditions et lève une exception — souvent IllegalArgumentException — si l'une d'entre elles n'est pas satisfaite. Par exemple, une méthode m prenant en argument un angle et demandant à ce que celui-ci soit dans l'intervalle \([0; 2\pi[\) pourrait commencer ainsi :

double m(double angle) {
  if (! (0 <= angle && angle < 2 * PI))
    throw new IllegalArgumentException("invalid angle");
  // …
}

L'interface Preconditions du paquetage ch.epfl.alpano a pour but de faciliter l'écriture de telles préconditions. En l'utilisant, la méthode ci-dessus pourrait être récrite ainsi :

import static ch.epfl.alpano.Preconditions.checkArgument;

double m(double angle) {
  checkArgument(0 <= angle && angle < 2 * PI,
                "invalid angle");
  // …
}

Pour ce faire, l'interface Preconditions offre les deux méthodes publiques et statiques suivantes :

  • void checkArgument(boolean b), qui lève l'exception IllegalArgumentException, sans message attaché, si son argument est faux, et ne fait rien sinon,
  • void checkArgument(boolean b, String message), qui lève l'exception IllegalArgumentException avec le message donné attaché si son argument est faux, et ne fait rien sinon.

3.3 Interface Distance

L'interface Distance contient des méthodes permettant de convertir des distances à la surface de la Terre de radians en mètres, et inversément. Elle possède un champ public et statique :

  • double EARTH_RADIUS, qui donne le rayon de la Terre, en mètres, soit 6371000,

ainsi que les méthodes publiques et statiques suivantes :

  • double toRadians(double distanceInMeters), qui convertit une distance à la surface de la Terre exprimée en mètres en l'angle correspondant, en radians,
  • double toMeters(double distanceInRadians), qui convertit un angle en radians en la distance correspondant à la surface de la Terre, en mètres.

3.4 Interface Math2

L'interface Math2 du paquetage ch.epfl.alpano contient différentes méthodes mathématiques utiles au projet. Elle joue un rôle similaire à la classe java.lang.Math de la bibliothèque Java, et lui sert de complément.

Notez qu'il aurait techniquement été possible de nommer cette interface Math au lieu de Math2, car elle est dans un paquetage différent de la classe Math de la bibliothèque Java. En pratique, il est malheureusement pénible d'utiliser deux classes ou interfaces ayant le même nom (non qualifié) dans un seul projet Java, raison pour laquelle nous utilisons Math2.

L'interface Math2 offre un unique champ statique :

  • double PI2, qui vaut 2*Math.PI, soit approximativement \(2\pi\),

et les méthodes statiques suivantes :

  • double sq(double x), qui retourne x élevé au carré,
  • double floorMod(double x, double y), qui retourne le reste de la division entière par défaut de x par y,
  • double haversin(double x), qui retourne le demi sinus verse de x,
  • double angularDistance(double a1, double a2), qui retourne la différence angulaire de a1 et a2,
  • double lerp(double y0, double y1, double x), qui retourne la valeur de f(x) obtenue par interpolation linéaire, sachant que f(0) vaut y0 et f(1) vaut y1 ; notez que x peut être quelconque,
  • double bilerp(double z00, double z10, double z01, double z11, double x, double y), qui retourne la valeur de f(x,y) obtenue par interpolation bilinéaire, sachant que f(0,0) vaut z00, f(1,0) vaut z10, f(0,1) vaut z01 et f(1,1) vaut z11 ; notez que x et y peuvent être quelconques,
  • double firstIntervalContainingRoot(DoubleUnaryOperator f, double minX, double maxX, double dX), qui cherche et retourne la borne inférieure du premier intervalle de taille dX contenant un zéro de la fonction f et compris entre minX et maxX ; si aucun zéro n'est trouvé, retourne Double.POSITIVE_INFINITY; l'interface DoubleUnaryOperator est décrite plus bas,
  • double improveRoot(DoubleUnaryOperator f, double x1, double x2, double epsilon), qui utilise la méthode de la dichotomie pour déterminer un intervalle compris entre x1 et x2, de taille inférieure ou égale à epsilon et contenant un zéro de f, et retourne sa borne inférieure ; lève l'exception IllegalArgumentException si f(x1) et f(x2) sont de même signe.

3.4.1 L'interface DoubleUnaryOperator

Le paquetage java.util.function de la bibliothèque standard Java contient plusieurs interfaces représentant des fonctions mathématiques de différents types. L'interface DoubleUnaryOperator représente une fonction à une variable réelle produisant un résultat réel, c-à-d de type \(\mathbb{R}\rightarrow \mathbb{R}\), et est définie ainsi :

package java.util.function;

public interface DoubleUnaryOperator {
  double applyAsDouble(double operand);
}

La méthode applyAsDouble permet d'appliquer la fonction mathématique représentée par le récepteur de la méthode (this) à l'argument donné et nommé operand ci-dessus.

Par exemple, la fonction \(f : \mathbb{R}\rightarrow\mathbb{R}\) définie par \(f(x) = \sin(x^2)\) peut être représentée au moyen d'une classe F définie ainsi2 :

import static java.lang.Math.sin;

public final class F implements DoubleUnaryOperator {
  @Override
  public double applyAsDouble(double x) {
    return sin(x * x);
  }
}

Une fois cette classe définie, il est possible de manipuler la fonction \(f\) comme une valeur, en créant une instance de la classe F. Par exemple :

DoubleUnaryOperator f = new F();
for (int x = -10; x <= 10; ++x) {
  double fOfX = f.applyAsDouble(x);
  System.out.println("f(" + x + ") = " + fOfX);
}

3.5 Interface Azimuth

L'interface Azimuth du paquetage ch.epfl.alpano regroupe les méthodes permettant de manipuler des nombres représentant des azimuts exprimés en radians. Elle possède les méthodes publiques et statiques suivantes :

  • boolean isCanonical(double azimuth), qui retourne vrai ssi son argument est un azimut « canonique », c-à-d compris dans l'intervalle \([0; 2\pi[\),
  • double canonicalize(double azimuth), qui retourne l'azimut canonique équivalent à celui passé en argument, c-à-d compris dans l'intervalle \([0; 2\pi[\),
  • double toMath(double azimuth), qui transforme un azimut en angle mathématique, c-à-d exprimé dans le sens antihoraire, ou lève l'exception IllegalArgumentException si son argument n'est pas un azimut canonique,
  • double fromMath(double angle), qui transforme un angle mathématique en azimut, c-à-d exprimé dans le sens horaire, ou lève l'exception IllegalArgumentException si l'argument n'est pas compris dans l'intervalle \([0; 2\pi[\),
  • String toOctantString(double azimuth, String n, String e, String s, String w), qui retourne une chaîne correspondant à l'octant dans lequel se trouve l'azimut donné, formée en combinant les chaînes n, e, s et w correspondant aux quatre points cardinaux (resp. nord, est, sud et ouest), comme décrit ci-dessous ; lève l'exception IllegalArgumentException si l'azimut donnée n'est pas canonique.

La méthode toOctantString a pour but de transformer un azimut en le nom de l'octant qui le contient. Chaque octant couvre une zone de 45° centrée sur chaque point cardinal ou intercardinal, comme illustré à la figure 2. Par exemple, appliquée à l'azimut \(\tfrac{\pi}{6}\) (30°) et aux chaînes "N", "E", "S" et "W" (dans cet ordre), elle retourne la chaîne "NE".

3.6 Tests

Pour vous aider à démarrer ce projet, nous vous fournissons exceptionnellement une archive Zip contenant des tests unitaires JUnit pour cette étape.

Pour pouvoir utiliser ces tests, il vous faut tout d'abord les importer dans votre projet en suivant les indications d'importation d'archive Zip dans Eclipse, puis ajouter la bibliothèque JUnit à votre projet, en suivant les explications à ce sujet.

3.7 Documentation

Une fois les tests exécutés avec succès, il vous reste à documenter la totalité des entités publiques (classes, attributs et méthodes) définies dans cette étape, au moyen de commentaires Javadoc, comme décrit dans le guide consacré à ce sujet. Vous pouvez écrire ces commentaires en français ou en anglais, en fonction de votre préférence, mais vous ne devez utiliser qu'une seule langue pour tout le projet.

4 Résumé

Pour cette étape, vous devez :

  • configurer Eclipse selon les indications données dans le document consacré à ce sujet,
  • écrires les interfaces Preconditions, Azimuth, Distance et Math2 selon les indications données plus haut,
  • vérifier que les tests que nous vous fournissons s'exécutent sans erreur, et dans le cas contraire, corriger votre code,
  • documenter la totalité des entités publiques que vous avez définies,
  • (optionnel mais vivement recommandé) rendre votre code au plus tard le 24 février 2017 à 16h30, via le système de rendu.

Ce premier rendu n'est pas noté, mais celui de la prochaine étape le sera. Dès lors, il vous est fortement conseillé de faire un rendu de test cette semaine afin de vous familiariser avec la procédure à suivre.

Notes de bas de page

1

Notez la différence subtile d'orthographe entre le français azimut et l'anglais azimuth.

2

Une syntaxe beaucoup plus concise, nommée lambda, existe désormais en Java pour définir une classe similaire à la classe F. Nous l'examinerons ultérieurement au cours.