Correction de géométrie
Notion de validité de la géométrie des entités

Une géométrie non valide est la cause principale de l'échec des requêtes spatiales.
Dans 90% des cas la réponse à la question “pourquoi mes requêtes me renvoient un message d'erreur du type 'TopologyException error' est : “un ou plusieurs des arguments passés sont invalides”. Ce qui nous conduit à nous demander : que signifie invalide et pourquoi est-ce important ?
La validité est surtout importante pour les polygones, qui définissent des surfaces et requièrent une bonne structuration.
Certaines des règles de validation des polygones semblent évidentes, et d'autres semblent arbitraires (et le sont vraiment) :
Les contours des polygones doivent être fermés.
Les contours qui définissent des trous doivent être inclus dans la zone définie par le contour extérieur.
Les contours ne doivent pas s'intersecter (ils ne doivent ni se croiser ni se toucher).
Les contours ne doivent pas toucher les autres contours, sauf en un point unique.
Les deux dernières règles font partie de la catégorie arbitraire.
PostGIS est conforme au standard OGC OpenGIS Specifications.
Les entités géométriques des bases PostGIS doivent ainsi être à la fois simples et valides.
Par exemple, calculer la surface d'un polygone comportant un trou à l'extérieur ou construire un polygone à partir d'une limite non simple n'a pas de sens.
Selon les spécifications de l'OGC, une géométrie simple est une géométrie qui ne comporte pas de points géométriques anormaux, comme des auto-intersections ou des auto-tangences, ce qui concerne essentiellement les points, les multi-points, les polylignes et les multi-polylignes.
La notion de géométrie valide concerne principalement les polygones et les multi-polygones et le standard définit les caractéristiques d'un polygone valide.

Un point est par nature simple, ayant une dimension égale à 0.
Un objet multi-points est simple si tous les points le composant sont distincts.
Une polyligne est simple si elle ne se recroise pas (les extrémités peuvent être confondues, auquel cas c'est un anneau et la polyligne est close).


Une multi-polyligne est simple si toutes les polylignes la composant sont elles-mêmes simples et si les intersections existant entre 2 polylignes se situent à une extrémité de ces éléments :


Un polygone est valide si ses limites, qui peuvent être constituées par un unique anneau extérieur (polygone plein) ou par un anneau extérieur et un ou plusieurs anneaux intérieurs (polygone à trous), ne comporte pas d'anneaux se croisant.
Un anneau peut intersecter la limite mais seulement en un point (pas le long d'un segment).
Un polygone ne doit pas comporter de lignes interrompues (les limites doivent être continues) ou de point de rebroussement (pic).
Les anneaux intérieurs doivent être entièrement contenus dans la limite extérieure du polygone.


Un multi-polygone est valide si et seulement si tous les polygones le composant sont valides et si aucun intérieur d'un polygone ne croise celui d'un autre.
Les limites de 2 polygones peuvent se toucher, mais seulement par un nombre fini de points (pas par une ligne).


Par défaut, PostGIS n'applique pas le test de validité géométrique lors de l'import d'entités géométriques, parce que le test de validité géométrique consomme beaucoup de temps processeur pour les géométries complexes, notamment les polygones.
Il faut donc mettre en œuvre différentes méthodes pour vérifier la validité de la géométrie des entités.

Exemple

Exemple :
Le polygone POLYGON((0 0, 0 1, 2 1, 2 2, 1 2, 1 0, 0 0)) n'est pas valide

Le contour externe est exactement en forme en 8 avec une intersection au milieu.
Notez que la fonction de rendu graphique est tout de même capable d'en afficher l'intérieur, donc visuellement cela ressemble bien à une “aire” : deux unités carré, donc une aire couplant ces deux unités.
Essayons maintenant de voir ce que pense la base de données de notre polygone en calculant sa surface :
SELECT ST_Area(ST_GeometryFromText('POLYGON((0 0, 0 1, 1 1, 2 1, 2 2, 1 2, 1 1, 1 0, 0 0))'));

Que ce passe-t-il ici ?
L'algorithme qui calcule la surface suppose que les contours ne s'intersectent pas.
Un contour normal devra toujours avoir une surface qui est bornée (l'intérieur) dans un sens de la ligne du contour (peu importe le sens).
Cependant, dans notre figure en 8, le contour externe est à droite de la ligne pour un lobe et à gauche pour l'autre.
Cela entraîne que les surfaces qui sont calculées pour chaque lobe annulent la précédente (l'une vaut 1 et l'autre -1) donc le résultat est une surface égale à 0.

- Détecter la validité
Dans l'exemple précédent nous avions un polygone que nous savions non-valide.
Comment déterminer les géométries non valides dans une table d'un million d'enregistrements ?
Avec la fonction ST_IsValid(geometry) utilisée avec notre polygone précédent, nous obtenons rapidement la réponse :
SELECT ST_IsValid(ST_GeometryFromText('POLYGON((0 0, 0 1, 1 1, 2 1, 2 2, 1 2, 1 1, 1 0, 0 0))'));
résultat : f (false)
Maintenant nous savons que la géométrie de l'entité n'est pas valide mais nous ne savons pas pourquoi.
Nous pouvons utiliser la fonction ST_IsValidReason(geometry) pour trouver la cause de non validité :
SELECT ST_IsValidReason(ST_GeometryFromText('POLYGON((0 0, 0 1, 1 1, 2 1, 2 2, 1 2, 1 1, 1 0, 0 0))'));
résultat : Self-intersection[1 1]
En plus de la cause d'invalifdité (auto-intersection), la localisation de la non validité (coordonnée (1 1)) est aussi renvoyée.
Nous pouvons aussi utiliser la fonction ST_IsValid(geometry) pour tester les tables comme dans l'exemple suivant :
SELECT ST_IsValidReason(geom
) FROM monschema.table WHERE Not ST_IsValid (
);geom

Détecter et corriger les erreurs géométriques par la pratique

Nous ébauchons ici la problématique des géométries invalides et des corrections envisageables.
Pour créer une table de géométries invalides dans le schéma travail:
exécuter le script SQL ci-dessous :
CREATE TABLE travail.invalidgeometry (id serial, type varchar(20), geom geometry(MULTIPOLYGON, 2154), PRIMARY KEY(id));
INSERT INTO travail.invalidgeometry (type, geom) VALUES ('Hole Outside Shell', ST_multi(ST_GeomFromText('POLYGON((465000 6700000, 465010 6700000, 465010 6700010, 465000 6700010, 465000 6700000), (465015 6700015, 465015 6700020, 465020 6700020, 465020 6700015, 465015 6700015))',2154)));
INSERT INTO travail.invalidgeometry (type, geom) VALUES ('Nested Holes', ST_multi(ST_GeomFromText('POLYGON((465030 6700000, 465040 6700000, 465040 6700010, 465030 6700010, 465030 6700000), (465032 6700002, 465032 6700008, 465038 6700008, 465038 6700002, 465032 6700002), (465033 6700003, 465033 6700007, 465037 6700007, 465037 6700003, 465033 6700003))',2154)));
INSERT INTO travail.invalidgeometry (type, geom) VALUES ('Dis. Interior', ST_Multi(ST_GeomFromText('POLYGON((465060 6700000, 465070 6700000,
465070 6700010, 465060 6700010, 465060 6700000), (465065 6700000, 465070 6700005, 465065 6700010, 465060 6700005, 465065 6700000))', 2154)));
INSERT INTO travail.invalidgeometry (type, geom) VALUES ('Self Intersect.', ST_multi(ST_GeomFromText('POLYGON((465090 6700000, 465100 6700010, 465090 6700010, 465100 6700000, 465090 6700000))',2154)));
INSERT INTO travail.invalidgeometry (type, geom) VALUES ('Ring Self Intersect.', ST_multi(ST_GeomFromText('POLYGON((465125 6700000, 465130 6700000, 465130 6700010, 465120 6700010, 465120 6700000, 465125 6700000, 465123 6700003, 465125 6700006, 465127 6700003, 465125 6700000))',2154)));
INSERT INTO travail.invalidgeometry (type, geom) VALUES ('Nested Shells', ST_multi(ST_GeomFromText('MULTIPOLYGON(((465150 6700000, 465160 6700000, 465160 6700010, 465150 6700010, 465150 6700000)),(( 465152 6700002, 465158 6700002, 465158 6700008, 465152 6700008, 465152 6700002)))',2154)));
Visualiser la table dans QGIS :
Chacun de ces objets est invalide.
Vérifions-le avec la requête suivante :
SELECT id, type, ST_IsValidReason(geom) FROM travail.invalidgeometry WHERE NOT ST_IsValid(geom);
qui nous renvoi :

Méthode : Correction avec ST_MakeValid()
Executer le script SQL :
CREATE TABLE travail.makevalidgeometry AS
(SELECT id, type, ST_MULTI(ST_MakeValid(geom))::geometry(MULTIPOLYGON, 2154) AS geom FROM travail.invalidgeometry WHERE NOT St_IsValid(geom));
qui renvoi :

Charger la nouvelle table dans QGIS :
Seul le dernier élément est différent :
constater que la requête :
SELECT id, type, ST_IsValidReason(geom) FROM travail.makevalidgeometry WHERE NOT ST_IsValid(geom);
Ne trouve plus d'erreur.
Exécuter :
SELECT st_AsText(geom) from travail.makevalidgeometry ;
qui renvoi :
"MULTIPOLYGON(((465000 6700000,465000 6700010,465010 6700010,465010 6700000,465000 6700000)),((465015 6700015,465015 6700020,465020 6700020,465020 6700015,465015 6700015)))"
"MULTIPOLYGON(((465030 6700000,465030 6700010,465040 6700010,465040 6700000,465030 6700000),(465032 6700002,465038 6700002,465038 6700008,465032 6700008,465032 6700002)),((465033 6700003,465033 6700007,465037 6700007,465037 6700003,465033 6700003)))"
"MULTIPOLYGON(((465065 6700000,465060 6700000,465060 6700005,465065 6700000)),((465060 6700005,465060 6700010,465065 6700010,465060 6700005)),((465065 6700010,465070 6700010,465070 6700005,465065 6700010)),((465070 6700005,465070 6700000,465065 6700000,4650 (...)"
"MULTIPOLYGON(((465090 6700000,465095 6700005,465100 6700000,465090 6700000)),((465095 6700005,465090 6700010,465100 6700010,465095 6700005)))"
"MULTIPOLYGON(((465125 6700000,465120 6700000,465120 6700010,465130 6700010,465130 6700000,465125 6700000),(465125 6700000,465127 6700003,465125 6700006,465123 6700003,465125 6700000)))"
"MULTIPOLYGON(((465150 6700000,465150 6700010,465160 6700010,465160 6700000,465150 6700000),(465152 6700002,465158 6700002,465158 6700008,465152 6700008,465152 6700002)))"
On peut comparer avec la table de départ.
SELECT st_AsText(geom) from travail.invalidgeometry
renvoi :
"MULTIPOLYGON(((465000 6700000,465010 6700000,465010 6700010,465000 6700010,465000 6700000),(465015 6700015,465015 6700020,465020 6700020,465020 6700015,465015 6700015)))"
"MULTIPOLYGON(((465030 6700000,465040 6700000,465040 6700010,465030 6700010,465030 6700000),(465032 6700002,465032 6700008,465038 6700008,465038 6700002,465032 6700002),(465033 6700003,465033 6700007,465037 6700007,465037 6700003,465033 6700003)))"
"MULTIPOLYGON(((465090 6700000,465100 6700010,465090 6700010,465100 6700000,465090 6700000)))"
"MULTIPOLYGON(((465125 6700000,465130 6700000,465130 6700010,465120 6700010,465120 6700000,465125 6700000,465123 6700003,465125 6700006,465127 6700003,465125 6700000)))"
"MULTIPOLYGON(((465150 6700000,465160 6700000,465160 6700010,465150 6700010,465150 6700000)),((465152 6700002,465158 6700002,465158 6700008,465152 6700008,465152 6700002)))"
"MULTIPOLYGON(((465060 6700000,465070 6700000,465070 6700010,465060 6700010,465060 6700000),(465065 6700000,465070 6700005,465065 6700010,465060 6700005,465065 6700000)))"
'Hole Outside Shell' (trou extérieur à l'enveloppe) : un seul polygone composé d'un trou en dehors du polygone de départ est devenu un multi-polygone composé de deux polygones.
'Nested Holes' (trous imbriqués) : est devenu un multipolygone composé d'un polygone avec trou et d'un deuxième polygone qui est au centre (le plus petit).
'Disconnected Interior' : (le trou touche le polygone en plus de 1 point) : est devenu un multi-polygone composé de 4 polygones en triangle.
'Self Intersection' (auto-intersection) : un multi-polygone composé de deux polygones en triangle.
'Ring Self Intersection'(anneau auto-intersectant, avec ici réduction de l'anneau en un point) : est devenu un polygone à trou (trou en contact en 1 point avec l'enveloppe ce qui est correct au sens de geos).
'Nested shell' (polygones imbriqués) : est devenu un polygone à trou.
Complément :
A la recherche d'une requête universelle...
La requête suivante permet de prendre en compte des cas de figure où ST_MakeVAlid() transforme des objets en collection de (multi)polygones, (multi)polyliges, ou (multi)point.
C'est le cas si, par exemple on part d'une couche avec un polygone comprenant un arc qui deviendra avec ST_Makevalid() une collection d'un polygone et d'une polyligne.
On peut utiliser :
update ma_table SET geom = ST_Multi(ST_CollectionExtract(st_simplify(ST_ForceCollection(ST_MakeValid(geom)),0),3))
st_simplify(geometry,0) permet de supprimer les nœuds en double.
ST_ForceCollection() permet de forcer le résultat comme une collection, même si St_MakeValid() ne produit pas de collection, afin de pouvoir utiliser après St_CollectionExtract()
On utilise ST_CollectionExtract(geometry,3) pour ne retenir dans cet exemple que les polygones de la collection.
et ST_Multi() pour forcer le résultat en multipolygones, même s'il n'y a qu'un seul polygone extrait.
Enfin pour ne pas simplifier inutilement des objets (par exemple linéaire dans notre exemple ou l'on recherche des polygones) on utilisera finalement la requête suivante :
update ma_table SET geom = st_multi(ST_Simplify(ST_Multi(ST_CollectionExtract(ST_ForceCollection(ST_MakeValid(geom)),3)),0)) WHERE ST_GeometryType(geom) like ' %Polygon' and St_invalid(geom)
Méthode : Correction par ST_Buffer(geom,0)
Une autre solution souvent proposée sur les forums est de corriger les erreurs de géométrie en réalisant un buffer nul.
Executer le script suivant :
CREATE TABLE travail.geometryvalidbuffer AS
(SELECT id, type, ST_Multi(ST_Buffer(geom,0))::geometry(MULTIPOLYGON, 2154) FROM travail.invalidgeometry WHERE NOT ST_IsValid(geom));
Charger cette couche dans QGIS :
Constater que les parties de polygones en jaune ont disparu en particulier pour les polygones en 'papillons'.
La méthode avec st_buffer(geom, 0) est donc une alternative, si la méthode st_makevalid() échoue (gros jeu de données), mais elle ne doit être utilisée que si on a déjà corrigé les polygones en ‘papillons'
Il est fortement conseillé de noter le nombre d'objets de la couche avant et après traitement pour une première vérification de pertinence.
Complément :
Il existe l'algorithme Réparer les géométries dans Processing (menu traitement).
Nous n'avons abordé ici que la problématique des géométries invalides au sens de l'osgeo. Mais il peut exister d'autres motif de correction comme des incohérences entres polygones d'une même couche (dépend des spécifications de saisie), comme des recouvrements (partiels ou non), des trous, des petits polygones (scories),...
Il existe d'autres solutions pour experts, comme l'utilisation de fonctions de GRASS (v.in.ogr et v.clean) . Le plugin vérificateur de géométrie qui est une extension principale de QGIS est désormais relativement stable (QGIS 3 et plus) et intéressant.
Complément : Pour en savoir plus...
consulter la page spécifique sur le site Géoinformation.