Etape 2 – Entités géométriques
1 Introduction
Le but de cette seconde étape est d'écrire les classes permettant de représenter les entités géométriques utilisées pour dessiner les cartes, à savoir :
- les lignes polygonales, ou polylignes, et
- les polygones à trous.
Les polylignes permettent de représenter les routes, les rivières, les lignes de chemin de fer, etc. Les polygones à trous, quant à eux, permettent de représenter les bâtiments, les lacs, les forêts, etc.
Ces deux entités géométriques sont bien entendu basées sur les points développés lors de l'étape précédente.
Il est important de noter que la terminologie utilisée ici, qui est celle de la géomatique, diffère de la terminologie mathématique, ce qui peut prêter à confusion. En particulier, ce que nous nommons plus bas polyligne fermée est nommé polygone en mathématique, tandis que ce que nous nommons polygone (à trous) correspond, dans sa forme générale, à un ensemble de polygones mathématiques. Gardez bien cela à l'esprit !
1.1 Polylignes
Une polyligne, ou ligne polygonale, est une ligne formée d'une séquence de segments de droite telle que le second point de chaque segment coïncide avec le premier point du segment suivant.
Une polyligne est spécifiée par la séquence ordonnée de ses sommets, chaque sommet étant un point du plan. Par exemple, la polyligne ci-dessous est spécifiée par la séquence de sommets \((V_0, V_1, V_2, V_3, V_4, V_5)\).
Figure 1 : Une polyligne (ouverte) à 6 sommets
Une polyligne est dite fermée si son premier et son dernier sommet sont reliés par un segment, ouverte sinon. Toute polyligne fermée englobe une région du plan, et deux opérations peuvent donc être définies sur ce type de polylignes :
- le calcul de l'aire, qui consiste à déterminer la surface de la région englobée par la polyligne fermée,
- le test d'intériorité, qui consiste à déterminer si un point se trouve ou non dans la région englobée par la polyligne fermée.
Ces deux opérations sont utiles à la transformation de données OpenStreetMap en données géométriques, comme nous le verrons lors d'une étape ultérieure, et sont donc décrites en détail plus bas. Avant cela, il importe toutefois de décrire deux primitives sur lesquelles se basent ces opérations, à savoir l'aire signée d'un triangle et la position d'un point par rapport à une droite.
Aire signée d'un triangle
Il existe plusieurs techniques pour calculer l'aire d'un triangle. La plus connue est probablement celle consistant à multiplier la base du triangle par sa hauteur puis à diviser le résultat par deux.
Toutefois, lorsqu'un triangle est représenté par ses trois sommets, ce qui est le cas des triangles considérés plus bas, une technique plus simple de calculer cette aire consiste à utiliser le produit vectoriel. Pour la comprendre, considérons le triangle \(\triangle V_0 V_1 V_2\) de la figure ci-dessous.
Figure 2 : Le triangle \(\triangle V_0 V_1 V_2\) et les vecteurs \(\overrightarrow{V_0 V_1}\) et \(\overrightarrow{V_0 V_2}\)
Les vecteurs \(\vec{v} = \overrightarrow{V_0V_1}\) et \(\vec{w} = \overrightarrow{V_0V_2}\) sont bidimensionnels, il n'est donc pas possible de calculer leur produit vectoriel, cette opération n'étant définie que pour les vecteurs tridimensionnels. On peut toutefois rendre ces vecteurs tridimensionnels en leur ajoutant une troisième composante nulle. On obtient alors les deux vecteurs \(\vec{v_3}\) et \(\vec{w_3}\) suivants, où \(x_i\) et \(y_i\) sont les coordonnées du sommet \(V_i\) :
\[\vec{v_3} = \left(\begin{array}{c} x_1 - x_0\\ y_1 - y_0\\ 0 \end{array} \right)\ \vec{w_3} = \left(\begin{array}{c} x_2 - x_0\\ y_2 - y_0\\ 0 \end{array} \right)\]
Le produit vectoriel de ces deux vecteurs, \(\vec{v_3}\times\vec{w_3}\), vaut :
\[\vec{v_3}\times\vec{w_3} = \left(\begin{array}{c} 0\\ 0\\ (x_1 - x_0)(y_2 - y_0) - (x_2 - x_0)(y_1 - y_0) \end{array}\right)\]
Une propriété du produit vectoriel de deux vecteurs est que sa norme est égale à l'aire du parallélogramme engendré par ces vecteurs. Dès lors, l'aire du triangle \(\triangle V_0 V_1 V_2\), notée \(A(\triangle V_0 V_1 V_2)\) peut s'obtenir en divisant la norme de ce produit vectoriel par deux :
\begin{align} A(\triangle V_0 V_1 V_2)&= \frac{1}{2}\|\vec{v_3}\times\vec{w_3}\|\\ &= \frac{1}{2}\left|(x_1 - x_0)(y_2 - y_0) - (x_2 - x_0)(y_1 - y_0)\right| \end{align}En supprimant la valeur absolue de la formule ci-dessous, on obtient une autre notion d'aire, que nous nommerons l'aire signée et que nous noterons \(A^±\). Cette aire signée est donc définie ainsi :
\[ A^{±}(\triangle V_0 V_1 V_2) = \frac{1}{2}\left((x_1 - x_0)(y_2 - y_0) - (x_2 - x_0)(y_1 - y_0)\right) \]
Comme son nom le suggère, l'aire signée peut être positive ou négative. Mais que signifie le signe ? En examinant les propriétés du produit vectoriel, on constate que ce signe est positif si et seulement si les sommets du triangle sont organisés dans le sens antihoraire1.
Ainsi, étant donnés les trois points de la figure 2, l'aire signée du triangle \(\triangle V_0 V_1 V_2\) est positive, tandis que l'aire signée du triangle \(\triangle V_0 V_2 V_1\) est négative. En valeur absolue, ces deux aires signées sont bien entendu égales. Elles sont donc liées par la relation suivante :
\[ A^{±}(\triangle V_0 V_1 V_2) = -A^±(\triangle V_0 V_2 V_1) \]
Bien entendu, toute rotation de la liste des sommets du triangle préserve le signe. On a donc également :
\[ A^{±}(\triangle V_0 V_1 V_2) = A^±(\triangle V_1 V_2 V_0) = A^±(\triangle V_2 V_0 V_1) > 0 \]
et :
\[ A^{±}(\triangle V_0 V_2 V_1) = A^±(\triangle V_2 V_1 V_0) = A^±(\triangle V_1 V_0 V_2) < 0 \]
L'utilité de cette notion d'aire signée n'est pas apparente au premier abord, mais deviendra évidente plus bas.
Position d'un point par rapport à une droite
Une ligne passant par deux points distincts \(P_1\) et \(P_2\) partitionne le plan en trois parties :
- les points se trouvant sur la ligne,
- les points se trouvant (strictement) à gauche de la ligne, lorsque celle-ci est parcourue de \(P_1\) à \(P_2\),
- les points se trouvant (strictement) à droite de la ligne, lorsque celle-ci est parcourue de \(P_1\) à \(P_2\).
Cette partition est illustrée sur l'extrait du plan ci-dessous, qui montre l'ensemble des points se trouvant à gauche de (en rouge), à droite de (en vert) et sur (en noir) la ligne passant par \(P_1\) et \(P_2\), lorsqu'elle est parcourue dans ce sens. Par exemple, le point \(P\) se trouve à gauche de cette ligne.
Figure 3 : Points à gauche de, à droite de ou sur la ligne \(P_1\), \(P_2\)
Pour déterminer la position d'un point \(P\) par rapport à une ligne orientée passant par \(P_1\) puis \(P_2\), on peut utiliser l'aire signée du triangle \(\triangle P P_1 P_2\). Comme cela a été dit plus haut, celle-ci est positive si et seulement si les points \(P\), \(P_1\) et \(P_2\) sont orientés dans le sens antihoraire, c-à-d si \(P\) est à gauche de la ligne \(P_1\), \(P_2\).
Donc, si \(P=(x, y)\) et \(P_i=(x_i, y_i)\), alors le point \(P\) est à gauche de la ligne \(P_1\), \(P_2\) si et seulement si :
\begin{align} & A^±(\triangle P P_1 P_2) > 0\\ \Leftrightarrow & \tfrac{1}{2}\left((x_1 - x)(y_2 - y) - (x_2 - x)(y_1 - y)\right) > 0\\ \Leftrightarrow & (x_1 - x)(y_2 - y) - (x_2 - x)(y_1 - y) > 0\\ \Leftrightarrow & (x_1 - x)(y_2 - y) > (x_2 - x)(y_1 - y) \end{align}Aire d'une polyligne fermée
Une manière simple et élégante de déterminer l'aire (signée) d'une polyligne fermée consiste à choisir un point \(P\) quelconque du plan puis à additionner les aires signées de tous les triangles formés par ce point et les deux extrémités de chaque segment de la polyligne. Pour illustrer cette procédure, considérons la polyligne fermée à 7 sommets — \(V_0\) à \(V_6\) — de la figure ci-dessous.
Figure 4 : Une polyligne fermée à 7 sommets
On peut maintenant choisir un point \(P\) quelconque du plan, par exemple situé en-dessous de la polyligne pour faciliter le dessin. Cela fait, il est possible de dessiner les 7 triangles composés du point \(P\) et des extrémités des segments de la polyligne, à savoir \(\triangle P V_0 V_1, \triangle P V_1 V_2, \ldots, \triangle P V_6 V_0\). Les quatre premiers d'entre-eux ont une aire signée négative, tandis que les trois derniers ont une aire signée positive. Cela est illustré dans les deux images de la figure ci-dessous, où les triangles à aire négative apparaissent en rouge, tandis que ceux à aire positive apparaissent en vert. Pour faciliter la lecture, ces deux types de triangles sont représentés sur des images séparées.
Figure 5 : Les 7 triangles permettant de calculer l'aire de la polyligne
Il est clair qu'en additionnant les aires (signées) de tous ces triangles on obtient l'aire de la polyligne. Notez qu'elle est ici positive, car ses sommets sont orientés dans le sens antihoraire. Si ceux-ci étaient orientés dans le sens horaire, l'aire obtenue serait négative.
Etant donné que le point \(P\) est arbitraire, on peut choisir d'utiliser l'origine du plan \(O = (0, 0)\) afin de simplifier les formules. L'aire signée de la polyligne fermée \(L=(V_0, V_1, \ldots, V_n)\) où \(V_i = (x_i, y_i)\) s'obtient alors simplement en sommant l'aire des triangles décrits plus haut :
\begin{align} A^±(L) &= \sum_{i=0}^{n} A^±(\triangle O V_i V_{i+1})\\ &= \sum_{i=0}^{n} \frac{1}{2}\left((x_i - 0)(y_{i+1} - 0) - (x_{i+1} - 0)(y_i - 0)\right)\\ &= \sum_{i=0}^{n} \frac{1}{2}\left(x_i y_{i+1} - x_{i+1} y_i\right)\\ &= \frac{1}{2} \sum_{i=0}^{n}\left(x_i y_{i+1} - x_{i+1} y_i\right) \end{align}Notez que les indices des sommets sont à prendre modulo \(n + 1\). Par exemple, le dernier terme de la somme ci-dessus est \(x_n y_{n+1} - x_{n+1} y_n\). Toutefois, le dernier sommet portant l'indice \(n\), \(x_{n+1}\) désigne en réalité \(x_0\). Il en va de même pour \(y_{n+1}\).
En réorganisant les termes de la formule ci-dessus, on obtient la version ci-dessous, plus rapide à calculer car comportant moins de multiplications :
\[ A^±(L) = \frac{1}{2}\sum_{i=0}^{n} x_i(y_{i+1} - y_{i-1}) \]
L'aire conventionnelle, c-à-d positive, s'obtient simplement en prenant la valeur absolue de l'aire signée :
\begin{align} A(L) &= \left|A^±(L)\right|\\ &= \frac{1}{2}\left|\sum_{i=0}^{n} x_i(y_{i+1} - y_{i-1})\right| \end{align}Test d'intériorité
Il existe plusieurs algorithmes permettant de tester si un point se trouve ou non à l'intérieur d'une polyligne fermée. Celui décrit ci-dessous utilise la notion d'indice d'une courbe par rapport à un point.
L'indice (winding number en anglais) d'une courbe fermée par rapport à un point est le nombre de tours, dans le sens antihoraire, que la courbe fait autour de ce point. L'animation ci-dessous illustre ce concept dans le cas d'une courbe (rouge) dont l'indice par rapport au point sur lequel se trouve l'observateur vaut 2. En effet, comme le montre l'animation, l'observateur tourne deux fois sur lui-même dans le sens antihoraire lorsqu'il suit des yeux un point parcourant la courbe.
Figure 6 : Indice d'une courbe (source : Wikimedia Commons)
L'indice permet de déterminer si un point se trouve à l'intérieur ou à l'extérieur de la surface délimitée par une courbe fermée. En effet, un point se trouve à l'extérieur d'une courbe si et seulement si l'indice de la courbe par rapport à ce point vaut 0.
Pour calculer l'indice d'une courbe fermée par rapport à un point \(P\), on peut faire l'observation suivante : si on trace un rayon infini à partir de \(P\) dans une direction quelconque, l'indice est donné par la somme des intersections signées entre la courbe et ce rayon. Cette somme se calcule en additionant 1 chaque fois que la courbe intersecte le rayon dans le sens antihoraire, et en soustrayant 1 chaque fois que la courbe intersecte le rayon dans le sens horaire.
La figure 7 ci-dessous illustre ce calcul pour la courbe et le point de la figure 6 plus haut. Le rayon \(R\) choisi est ici horizontal et il intersecte la courbe en quatre points nommés \(I_1\) à \(I_4\). Etant donné qu'en les points \(I_1\), \(I_3\) et \(I_4\) la courbe intersecte \(R\) dans le sens antihoraire, tandis qu'en \(I_2\) elle l'intersecte dans le sens horaire, l'indice vaut 3 – 1 = 2.
Figure 7 : Indice d'une courbe par comptage d'intersections
Cette technique du calcul de l'indice est particulièrement simple à mettre en œuvre dans le cas où la courbe est une polyligne fermée, cas qui nous intéresse ici. En effet, il suffit alors de trouver tous les segments de la polyligne qui intersectent le rayon horizontal partant du point, puis de déterminer pour chacun d'eux le sens de l'intersection afin de calculer la somme des intersections signées et d'obtenir ainsi l'indice recherché.
En pratique, il est en fait plus simple de trouver tous les segments qui intersectent la ligne (et pas le rayon) horizontale passant par le point \(P\), puis de déterminer le sens de chaque intersection. Ensuite, on ajoute 1 à l'indice si et seulement si l'intersection est à la montée et \(P\) se trouve à gauche du segment. De même, on soustrait 1 à l'indice si et seulement si l'intersection est à la descente et \(P\) se trouve à droite du segment, c-à-d à gauche du segment inversé.
L'algorithme ci-dessous utilise cette technique pour déterminer si un point \(P\) se trouve ou non à l'intérieur d'une polyligne fermée \(L\), en calculant d'abord l'indice de \(L\) par rapport à \(P\) avant de le comparer avec 0.
1: est_intérieur(P, L) : 2: indice = 0 3: pour chaque segment S = (P1, P2) de L : 4: si P1.y <= P.y : 5: si P2.y > P.y et P à gauche de (P1, P2) : 6: indice = indice + 1 7: sinon : 8: si P2.y <= P.y et P à gauche de (P2, P1) : 9: indice = indice - 1 10: retourne (indice != 0)
1.2 Polygones à trous
Un polygone à trou est un polygone formé d'une enveloppe et d'un ensemble quelconque, et éventuellement vide, de trous. L'enveloppe et les trous sont des polylignes fermées, et tous les trous doivent être totalement contenus dans l'enveloppe. C'est-à-dire qu'aucun point du plan ne doit se trouver à l'intérieur d'un trou sans se trouver à l'intérieur de l'enveloppe.
La figure ci-dessous montre un polygone composé d'une enveloppe rectangulaire (trait gras) et de 14 trous.
Figure 8 : Polygone à enveloppe rectangulaire doté de 14 trous
2 Mise en œuvre Java
Les polylignes et les polygones sont modélisés par des classe du paquetage ch.epfl.imhof.geometry
qui contient déjà la classe Point
de l'étape précédente.
La mise en œuvre des polylignes est partagée entre trois classes : PolyLine
est une classe mère abstraite représentant les polylignes quelconques (c-à-d ouvertes ou fermées), tandis que ses sous-classes OpenPolyLine
et ClosedPolyLine
représentent respectivement les polylignes ouvertes et fermées.
2.1 PolyLine
La classe PolyLine
du paquetage ch.epfl.imhof.geometry
, publique, abstraite et immuable, représente une polyligne. Elle est dotée d'un unique constructeur public :
PolyLine(List<Point> points)
, qui construit une polyligne avec les sommets donnés. Lève l'exceptionIllegalArgumentException
si la liste des sommets est vide.
La classe PolyLine
possède de plus une méthode publique abstraite permettant de savoir si elle est fermée ou non :
boolean isClosed()
, qui retourne vrai si et seulement si la polyligne est fermée.
Finalement, la classe PolyLine
offre les méthodes publiques suivantes :
List<Point> points()
, qui retourne la liste des sommets de la polyligne.Point firstPoint()
, qui retourne le premier sommet de la polyligne.
2.2 OpenPolyLine
La classe OpenPolyLine
du paquetage ch.epfl.imhof.geometry
, publique, finale et immuable, hérite de la classe PolyLine
et représente une polyligne ouverte. Elle offre un constructeur public similaire à celui de PolyLine
, à savoir :
OpenPolyLine(List<Point> points)
, qui construit une polyligne ouverte de sommets donnés.
Bien entendu, elle fournit de plus une version concrète de la méthode isClosed
héritée de sa super-classe.
2.3 ClosedPolyLine
La classe ClosedPolyLine
du paquetage ch.epfl.imhof.geometry
, publique, finale et immuable, hérite de la classe PolyLine
et représente une polyligne fermée. Tout comme OpenPolyLine
, elle offre un constructeur public similaire à celui de sa super-classe :
ClosedPolyLine(List<Point> points)
, qui construit une polyligne fermée ayant les sommets donnés.
Elle fournit également une version concrète de la méthode isClosed
héritée de sa super-classe. De plus, elle offre les méthodes publiques ci-dessous :
double area()
, qui retourne l'aire (toujours positive !) de la polyligne.boolean containsPoint(Point p)
, qui retourne vrai si et seulement si le point donné est à l'intérieur de la polyligne.
En plus de ces méthodes publiques, il est conseillé d'écrire deux méthodes privées, la première permettant de déterminer si un point se trouve à gauche d'une ligne définie par deux points, la seconde permettant d'obtenir un sommet d'indice généralisé.
Par indice généralisé, on entend un indice qui n'est pas forcément compris dans l'intervalle \([0;n]\) pour une polyligne à \(n+1\) sommets. Ces indices apparaissent lors du calcul de l'aire et du test d'intériorité, et doivent être ramenés à l'intervalle \([0;n]\) faute de quoi une erreur se produit. Par exemple, pour une polyligne à \(n+1\) sommets, l'indice généralisé \(n+1\) doit être transformé en \(0\), tandis que l'indice \(-1\) doit être transformé en \(n\).
Pour faire cette transformation, on pourrait être tenté d'utiliser l'opérateur %
de Java, qui calcule le reste de la division entière d'un entier par un autre. Cet opérateur retourne effectivement la valeur attendue pour tous les indices généralisés positifs, mais malheureusement pas pour les indices généralisés négatifs. Par exemple -1 % 3
vaut –1 et pas 2 comme on pourrait s'y attendre.
Heureusement, la méthode floorMod
de la classe java.lang.Math
permet de résoudre ce problème. Elle est similaire à l'opérateur %
mais retourne une valeur qui a toujours le signe de son second argument. Par exemple, floorMod(-1, 3)
vaut 2. C'est donc elle qu'il convient d'utiliser pour transformer un indice généralisé en indice valide.
2.4 PolyLine.Builder
La classe PolyLine.Builder
, imbriquée statiquement dans la classe PolyLine
, publique et finale, sert de bâtisseur aux deux sous-classes de PolyLine
et permet de construire une polyligne en plusieurs étapes. Cette classe ne possède pas d'autre constructeur que celui par défaut, mais offre les méthodes publiques suivantes :
void addPoint(Point newPoint)
, qui ajoute le point donné à la (fin de la) liste des sommets de la polyligne en cours de construction.OpenPolyLine buildOpen()
, qui construit et retourne une polyligne ouverte avec les points ajoutés jusqu'à présent.ClosedPolyLine buildClosed()
, qui construit et retourne une polyligne fermée avec les points ajoutés jusqu'à présent.
2.5 Polygon
La classe Polygon
du paquetage ch.epfl.imhof.geometry
, publique, finale et immuable, représente un polygone à trous. Elle est dotée des deux constructeurs publics suivants :
Polygon(ClosedPolyLine shell, List<ClosedPolyLine> holes)
, qui construit un polygone avec l'enveloppe et les trous donnés. Idéalement, le constructeur devrait vérifier que les trous sont contenus dans l'enveloppe, mais cela n'étant pas trivial, nous omettrons cette vérification.Polygon(ClosedPolyLine shell)
, qui construit un polygone avec l'enveloppe donnée, sans trous.
Elle offre de plus les deux méthodes d'accès ci-dessous :
ClosedPolyLine shell()
, qui retourne l'enveloppe.List<ClosedPolyLine> holes()
, qui retourne la liste des trous.
2.6 Tests
Pour cette seconde étape, nous vous fournissons à nouveau — mais pour la dernière fois — des tests unitaires contenus dans une archive Zip. Vous pouvez les importer dans votre projet en suivant les instructions données sur cette page.
Attention : nous ne garantissons pas l'exhaustivité de ces tests et nous nous réservons le droit d'utiliser des tests plus complets que ceux-ci pour noter votre projet.
3 Résumé
Pour cette étape, vous devez :
- écrire les classes
PolyLine
,OpenPolyLine
,ClosedPolyLine
,PolyLine.Builder
etPolygon
en fonction des spécifications 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,
- rendre votre code au plus tard le 27 février 2015 à 16h00, via le système de rendu (ce rendu est un rendu mineur et compte donc pour la note finale).
Attention : n'attendez surtout pas le dernier moment pour effectuer votre rendu, car vous n'êtes pas à l'abri d'imprévus et aucun retard, aussi insignifiant soit-il, ne sera toléré !
Notes de bas de page
Le sens antihoraire est le sens contraire des aiguilles d'une montre, tandis que le sens horaire est le sens des aiguilles d'une montre.