Éditer sur GitHub

TP - Mise en oeuvre des patrons de conception avec des classes géométriques

Introduction

L'objectif de ce TP est de s'exercer à mettre en oeuvre des patrons de conception via la création d'une petite bibliothèque de manipulation des géométries OGC :

Géométrie OGC

Vous noterez toutefois que ceci n'est qu'un exercice :

Démarrage

Mise en garde

Vous devrez impérativement :

Pour ce faire, il vous est vivement conseillé de :

0.1 - Coordinate (2D)

Objectif : Préparation du TP, principe de base, encapsulation

Créer une classe Coordinate permettant de représenter une position en 2D à l'aide d'un couple x,y :

Schéma UML

Nous soulignerons que :

Exemple d'utilisation :

public class CoordinateTest {

    public static final double EPSILON = 1.0e-15;

    @Test
    public void testCoordinateXY() {
        Coordinate c = new Coordinate(3.0, 4.0);
        assertEquals(3.0, c.getX(), EPSILON);
        assertEquals(4.0, c.getY(), EPSILON);
        assertFalse(c.isEmpty());
        assertEquals("[3.0,4.0]", c.toString());
    }

}

0.2 - Geometry, Point et LineString

Objectif : Préparation du TP, utilisation d'interface, encapsulation

Implémenter les trois classes suivantes illustrées sur le schémas ci-après :

Schéma UML

Remarques :

0.3 - Geometry.isEmpty()

Objectif : Bonne pratique NonNullObject

Dans la question précédente, nous remarquons que nous avons des choix à faire dans les constructeurs par défaut de Point et LineString.

Afin d'éviter d'avoir à tester des coordinate ou points null, nous allons ajouter le concept de géométrie vide :

Schéma UML

Remarque :

0.4 - Geometry.translate(dx,dy)

Objectif : Exploiter une interface pour réaliser un traitement spécifique

Ajouter une méthode de permettant de translater une géométrie.

Schéma UML

Remarque : Vous serez amené à créer une nouvelle Coordinate pour l'implémentation dans Point.

0.5 - Geometry.clone()

Objectif : Patron de conception Prototype

En introduisant la fonction précédente, nous avons renoncé à l'idée d'avoir des géométries immuable (non modifiable après construction).

Nous allons donc ajouter une méthode clone() permettant de récupérer une copie d'une géométrie :

Schéma UML

Exemple d'utilisation :

Geometry copy = g.clone();
copy.translate(10.0,10.0);
//... "g" n'est pas modifiée

Remarques :

0.6 - Envelope et EnvelopeBuilder

Objectif : Patron de conception Builder

Nous souhaitons calculer l'emprise d'une géométrie (la bbox). La logique de calcul de min/max en oeuvre étant assez complexe, nous ne souhaitons pas l'implémenter dans les classes Point et LineString.

Nous allons donc procéder comme suit :

Schéma UML

Exemple d'utilisation :

EnvelopeBuilder builder = new EnvelopeBuilder();
builder.insert(new Coordinate(0.0,1.0));
builder.insert(new Coordinate(2.0,0.0));
builder.insert(new Coordinate(1.0,3.0));
Envelope result = builder.build();

Remarques :

List<Double> xVals = new ArrayList<Double>();
xVals.add(1.0);
xVals.add(-1.0);
xVals.add(3.0);
double xmin = Collections.min(xVals);
double xmax = Collections.max(xVals);
Assert.assertEquals(-1.0,xmin,EPSILON);
Assert.assertEquals(3.0,xmax,EPSILON);

Cette approche ne sera pas "optimale", mais elle peut être un premier jet permettant la mise en oeuvre des tests avant optimisation.

0.7 - Geometry.getEnvelope() : Envelope

Objectif : Facade sur EnvelopeBuilder

Ajouter une méthode utilitaire sur Geometry pour récupérer facilement l'enveloppe comme suit :

Schéma UML

0.8 - WktWriter

Objectif : Mesurer l'intérêt d'une conception propre et de GeometryVisitor dans les questions suivantes

Ajouter une classe WktWriter avec une méthode permettant de convertir une géométrie au format WKT qui prendra par exemple les formes suivantes :

POINT EMPTY
POINT(3.0 4.0)
LINESTRING EMPTY
LINESTRING(0.0 0.0,1.0 1.0,5.0 5.0)

Schéma UML

Exemple d'utilisation :

Geometry g = new Point(new Coordinate(3.0,4.0));
WktWriter writer = new WktWriter();
assertEquals("POINT(3.0 4.0)", writer.write(g));

Remarques :

if ( geometry instanceof Point ){
    Point point = (Point)geometry;
    // traiter le cas Point
}else if ( geometry instanceof LineString ){
    LineString lineString = (LineString)geometry;
    // traiter le cas LineString
}else{
    throw new RuntimeException("geometry type not supported");
}

0.9 - GeometryVisitor

Objectif : Patron de conception Visitor, prise en main

Schéma UML

Exemple d'utilisation :

LogGeometryVisitor visitor = new LogGeometryVisitor();
Geometry geometry = new Point(new Coordinate(3.0,4.0));
geometry.accept(visitor);

Pour tester les écritures dans la console de LogGeometryVisitor, nous remarquerons que System.out est de type PrintStream et qu'il est possible d'écrire dans une chaîne de caractère plutôt que dans la console en procédant comme suit :

ByteArrayOutputStream os = new ByteArrayOutputStream();
PrintStream out = new PrintStream(os);
LogGeometryVisitor visitor = new LogGeometryVisitor(out);
geometry.accept(visitor);
// result contiendra ce qui est écrit dans la console
String result = os.toString("UTF8");

0.10 - WktVisitor

Objectif : Patron de conception Visitor, mise en oeuvre dans un cas concret

Reprendre l'implémentation de WktWriter sous la forme d'un GeometryVisitor en implémentant une classe WktVisitor.

Schéma UML

Exemple d'utilisation :

WktVisitor visitor = new WktVisitor();
Geometry geometry = new Point(new Coordinate(3.0,4.0));
geometry.accept(visitor);
assertEquals( "POINT(3.0 4.0)", visitor.getResult() );

Remarque : Au niveau de visit, on écrira dans la variable membre buffer à l'aide de append de StringBuilder. Au niveau de getResult(), on récupérera la chaîne du buffer à l'aide de toString de StringBuilder.

0.11 - Geometry.asText()

Objectif : Patron de conception Facade, héritage à trois niveau avec interface et abstract.

A l'aide de AbstractGeometry et WktVisitor :

Schéma UML

Remarque : Il faudra redéclarer la méthode clone() au niveau de AbstractGeometry.

0.12 - EnvelopeBuilder en tant que GeometryVisitor

Objectif : Visitor, refactoring (extraction de l'implémentation d'une fonctionnalité)

Schéma UML

0.13 - GeometryWithCachedEnvelope

Objectif : Patron de conception Decorator

Schéma UML

Exemple d'utilisation :

Geometry g = new Point(new Coordinate(3.0,3.0));
// décoration
g = new GeometryWithCachedEnvelope(g);
Envelope a = g.getEnvelope() ; // calcul et stockage dans cachedEnvelope
Envelope b = g.getEnvelope() ; // renvoi de cachedEnvelope
assertSame(a,b);

Remarque : on traitera l'invalidation du cache en cas de modification de la géométrie originale à la prochaine question.

0.14 - GeometryListener

Objectif : Patron de conception Observable

Pour être en mesure d'invalider l'enveloppe précalculée en cas de modification d'une géométrie, nous allons mettre en place un mécanisme d'événement :

Schéma UML

Remarque : translate étant la seule fonction capable de modifier une géométrie, il serait actuellement possible de se contenter d'invalider l'enveloppe en surchargeant translate dans GeometryWithCachedEnvelope pour nettoyer au passage le cache. Rien ne garanti toutefois que translate reste la seule fonction à même de modifier une géométrie et que toutes ces fonctions restent déclarées au niveau Geometry.

0.15 - GeometryCollection

Objectif : Patron de conception Composite, Refactoring

Ajouter une classe GeometryCollection représentant une géométrie multiple, adapter les autres fonctionnalitées.

Schéma UML

Remarque :

GEOMETRYCOLLECTION EMPTY
GEOMETRYCOLLECTION(POINT(3.0 4.0),LINESTRING(0.0 0.0,1.0 1.0,5.0 5.0))

0.16 - GeometryVisitor renvoyant un résultat

Objectif : Exploiter les classes génériques, visiteur renvoyant directement un résultat.

Pour avoir la capacité de renvoyer des résultats avec des types variables :

LengthVisitor<Double> visitor = new LengthVisitor<Double>();
Double result = geometry.accept(visitor);

Aller plus loin...

Pour blinder votre TP :

Se demander par exemple quel serait l'impact de l'ajout d'un paramètre optionnel pour contrôler le nombre de décimales (Indice : Vous avez le droit de définir une méthode privée writeCoordinate).

Indice : En matérialisant le concept mathématique que vous manipulez dans une classe Interval, vous encapsulerez efficacement le calcul d'un lower et d'un upper avec une méthode expandToInclude).

Pour prendre du recul :