Expertises Topologie - Claude et Qgis
Contexte PLU
Pour une couche de zones PLU topologiquement correcte, trois règles sont non-négociables :
- Pas de gaps — tout le territoire communal doit être couvert, sans trou entre zones
- Pas d'overlaps — une parcelle n'appartient qu'à une seule zone
- Géométries valides — pas de self-intersection, pas de géométrie nulle
Ces trois vérifications + corrections sont pilotables intégralement depuis Claude Code via le MCP QGIS, sans manipulation manuelle.
Bloc 1 — Configuration du snapping avant saisie
À lancer en début de session d'édition sur une couche PLU. Configure l'environnement QGIS pour que la saisie soit topologiquement propre dès le départ.
from qgis.core import QgsSnappingConfig, QgsTolerance, QgsProject
proj = QgsProject.instance()
snap_cfg = proj.snappingConfig()
snap_cfg.setEnabled(True)
snap_cfg.setMode(QgsSnappingConfig.SnappingMode.AllLayers)
snap_cfg.setType(QgsSnappingConfig.SnappingType.VertexAndSegment)
snap_cfg.setTolerance(10.0)
snap_cfg.setUnits(QgsTolerance.UnitType.Pixels)
snap_cfg.setIntersectionSnapping(True) # accrochage sur les intersections
proj.setSnappingConfig(snap_cfg)
proj.setTopologicalEditing(True) # partage de noeuds entre couches
proj.setAvoidIntersectionsMode(
QgsProject.AvoidIntersectionsMode.AvoidIntersectionsLayers
) # évite automatiquement les overlaps lors de la saisie
Paramètres clés :
| Paramètre | Valeur | Effet |
|---|---|---|
AllLayers |
mode | snap sur toutes les couches visibles |
VertexAndSegment |
type | accroche vertex ET bord |
10 px |
tolérance | adapté à la saisie courante PLU |
TopologicalEditing |
True | noeuds partagés entre polygones adjacents |
AvoidIntersectionsLayers |
mode | saisie d'un polygone qui "découpe" automatiquement ses voisins |
Piège : setType() attend QgsSnappingConfig.SnappingType — pas Qgis.SnappingType ni Qgis.SnappingTypes.
Bloc 2 — Audit topologique
Routine complète : validité + overlaps + gaps sur n'importe quelle couche polygone.
import processing
from qgis.core import QgsProject
def audit_topo(layer, unique_id='fid', gap_threshold=0, min_overlap_area=0):
"""
Audit topologique d'une couche polygone PLU.
Retourne un dict avec les couches d'erreurs et un résumé.
"""
rapport = {}
# 1. Validité géométrique (GEOS)
r_val = processing.run("native:checkvalidity", {
'INPUT_LAYER': layer, # NOTE : INPUT_LAYER, pas INPUT
'METHOD': 2, # GEOS
'IGNORE_RING_SELF_INTERSECTION': False,
'VALID_OUTPUT': 'memory:',
'INVALID_OUTPUT': 'memory:',
'ERROR_OUTPUT': 'memory:'
})
rapport['validite'] = {
'valides': r_val['VALID_COUNT'],
'invalides': r_val['INVALID_COUNT'],
'erreurs': r_val['ERROR_COUNT'],
'layer_invalides': r_val['INVALID_OUTPUT'],
'layer_erreurs': r_val['ERROR_OUTPUT']
}
# 2. Overlaps
r_ov = processing.run("native:checkgeometryoverlap", {
'INPUT': layer,
'UNIQUE_ID': unique_id,
'MIN_OVERLAP_AREA': min_overlap_area,
'TOLERANCE': 8,
'ERRORS': 'memory:',
'OUTPUT': 'memory:'
})
rapport['overlaps'] = {
'count': r_ov['ERRORS'].featureCount(),
'layer_errors': r_ov['ERRORS'],
'layer_features': r_ov['OUTPUT']
}
# 3. Gaps
r_gap = processing.run("native:checkgeometrygap", {
'INPUT': layer,
'UNIQUE_ID': unique_id,
'GAP_THRESHOLD': gap_threshold,
'TOLERANCE': 8,
'NEIGHBORS': 'memory:',
'ERRORS': 'memory:',
'OUTPUT': 'memory:'
})
rapport['gaps'] = {
'count': r_gap['OUTPUT'].featureCount(),
'layer_gaps': r_gap['OUTPUT'],
'layer_neighbors': r_gap['NEIGHBORS'], # requis pour fixgeometrygap
'layer_errors': r_gap['ERRORS']
}
# Résumé lisible
print(f"=== Audit topo : {layer.name()} ({layer.featureCount()} features) ===")
print(f"Validité : {rapport['validite']['valides']} OK / "
f"{rapport['validite']['invalides']} invalides")
print(f"Overlaps : {rapport['overlaps']['count']} erreur(s)")
print(f"Gaps : {rapport['gaps']['count']} trou(s)")
ok = (rapport['validite']['invalides'] == 0 and
rapport['overlaps']['count'] == 0 and
rapport['gaps']['count'] == 0)
print(f"Résultat : {'PROPRE' if ok else 'ERREURS DETECTEES'}")
return rapport
Usage :
from qgis.core import QgsProject
layer = QgsProject.instance().mapLayersByName("zones_plu")[0]
rapport = audit_topo(layer, unique_id='fid')
Piège critique : native:checkvalidity → paramètre INPUT_LAYER (pas INPUT). Erreur silencieuse sinon.
Bloc 3 — Correction automatique
S'appuie sur les couches d'erreurs produites par le Bloc 2.
3a. Corriger les overlaps
r_fix_ov = processing.run("native:fixgeometryoverlap", {
'INPUT': layer,
'ERRORS': rapport['overlaps']['layer_errors'],
'UNIQUE_ID': 'fid',
'OVERLAP_FEATURE_UNIQUE_IDX': 'gc_overlap_fid', # champ produit par checkgeometryoverlap
'ERROR_VALUE_ID': 'gc_error',
'OUTPUT': 'memory:',
'REPORT': 'memory:',
'TOLERANCE': 8
})
layer_corrige = r_fix_ov['OUTPUT']
print(f"Overlaps corrigés : {r_fix_ov['REPORT'].featureCount()} opérations")
3b. Combler les gaps
3 méthodes disponibles :
| Valeur | Méthode | Usage recommandé |
|---|---|---|
0 |
Ajouter à la plus longue bordure en commun | PLU — zone voisine qui partage le plus de frontière |
1 |
Créer une nouvelle entité | si le gap est une zone à part entière |
2 |
Ajouter à la plus grande zone voisine | quand la surface prime |
r_fix_gap = processing.run("native:fixgeometrygap", {
'INPUT': layer,
'NEIGHBORS': rapport['gaps']['layer_neighbors'], # issu de checkgeometrygap
'GAPS': rapport['gaps']['layer_gaps'],
'METHOD': 0, # 0 = plus longue bordure (recommandé PLU)
'UNIQUE_ID': 'fid',
'ERROR_ID_IDX': 'gc_errorid',
'OUTPUT': 'memory:',
'REPORT': 'memory:',
'TOLERANCE': 8
})
layer_sans_gap = r_fix_gap['OUTPUT']
print(f"Gaps comblés : {r_fix_gap['REPORT'].featureCount()} opérations")
3c. Corriger les géométries invalides (self-intersections, anneaux)
r_fix_val = processing.run("native:fixgeometries", {
'INPUT': rapport['validite']['layer_invalides'],
'METHOD': 1, # 1 = structure linéaire (robuste)
'OUTPUT': 'memory:'
})
layer_valide = r_fix_val['OUTPUT']
Bloc 4 — Accrochage post-saisie (snapgeometries)
Quand une couche a été saisie sans snapping actif, ou importée avec de micro-décalages entre polygones adjacents. Accroche les géométries d'une couche sur une couche de référence (ex : limites communales, autre zonage).
8 comportements disponibles :
| Valeur | Comportement | Usage |
|---|---|---|
0 |
Aligner les nœuds, ajoute sommets si besoin | défaut recommandé |
1 |
Point le plus proche, ajoute sommets | si nœuds mal alignés |
2 |
Aligner les nœuds, sans ajout de sommet | quand on veut conserver la densité |
4 |
Déplacer uniquement les extrémités (nœuds) | micro-corrections de bord |
6 |
Extrémités sur extrémités uniquement | lignes/filaires |
r_snap = processing.run("native:snapgeometries", {
'INPUT': layer_a_corriger,
'REFERENCE_LAYER': layer_reference,
'TOLERANCE': 0.5, # en unités de la couche (mètres si Lambert 93)
'BEHAVIOR': 0,
'OUTPUT': 'memory:'
})
layer_snapped = r_snap['OUTPUT']
Recommandation PLU : tolérance 0.1–0.5 m en EPSG:2154 (Lambert 93). Trop grande → déformation des géométries.
Routine principale — Audit + Rapport
Fonction complète prête à l'emploi. Appeler par Claude en une seule commande.
import processing
from qgis.core import QgsProject, QgsSnappingConfig, QgsTolerance
def configurer_snapping_plu():
proj = QgsProject.instance()
snap_cfg = proj.snappingConfig()
snap_cfg.setEnabled(True)
snap_cfg.setMode(QgsSnappingConfig.SnappingMode.AllLayers)
snap_cfg.setType(QgsSnappingConfig.SnappingType.VertexAndSegment)
snap_cfg.setTolerance(10.0)
snap_cfg.setUnits(QgsTolerance.UnitType.Pixels)
snap_cfg.setIntersectionSnapping(True)
proj.setSnappingConfig(snap_cfg)
proj.setTopologicalEditing(True)
proj.setAvoidIntersectionsMode(
QgsProject.AvoidIntersectionsMode.AvoidIntersectionsLayers)
print("Snapping PLU configuré")
def audit_topo(layer, unique_id='fid', gap_threshold=0, min_overlap_area=0):
r_val = processing.run("native:checkvalidity", {
'INPUT_LAYER': layer, 'METHOD': 2,
'IGNORE_RING_SELF_INTERSECTION': False,
'VALID_OUTPUT': 'memory:', 'INVALID_OUTPUT': 'memory:', 'ERROR_OUTPUT': 'memory:'
})
r_ov = processing.run("native:checkgeometryoverlap", {
'INPUT': layer, 'UNIQUE_ID': unique_id,
'MIN_OVERLAP_AREA': min_overlap_area, 'TOLERANCE': 8,
'ERRORS': 'memory:', 'OUTPUT': 'memory:'
})
r_gap = processing.run("native:checkgeometrygap", {
'INPUT': layer, 'UNIQUE_ID': unique_id,
'GAP_THRESHOLD': gap_threshold, 'TOLERANCE': 8,
'NEIGHBORS': 'memory:', 'ERRORS': 'memory:', 'OUTPUT': 'memory:'
})
rapport = {
'layer': layer,
'unique_id': unique_id,
'validite': {
'valides': r_val['VALID_COUNT'],
'invalides': r_val['INVALID_COUNT'],
'layer_invalides': r_val['INVALID_OUTPUT']
},
'overlaps': {
'count': r_ov['ERRORS'].featureCount(),
'layer_errors': r_ov['ERRORS'],
'layer_features': r_ov['OUTPUT']
},
'gaps': {
'count': r_gap['OUTPUT'].featureCount(),
'layer_gaps': r_gap['OUTPUT'],
'layer_neighbors': r_gap['NEIGHBORS'],
'layer_errors': r_gap['ERRORS']
}
}
ok = (r_val['INVALID_COUNT'] == 0 and
r_ov['ERRORS'].featureCount() == 0 and
r_gap['OUTPUT'].featureCount() == 0)
print(f"\n=== Audit topo : {layer.name()} ===")
print(f" Géométries invalides : {r_val['INVALID_COUNT']}")
print(f" Overlaps : {r_ov['ERRORS'].featureCount()}")
print(f" Gaps : {r_gap['OUTPUT'].featureCount()}")
print(f" => {'PROPRE' if ok else 'ERREURS — voir rapport'}")
return rapport
# Usage type
# layer = QgsProject.instance().mapLayersByName("zones_plu")[0]
# configurer_snapping_plu()
# rapport = audit_topo(layer)
Pièges à retenir
| Sujet | Constat |
|---|---|
INPUT_LAYER vs INPUT |
native:checkvalidity exige INPUT_LAYER, tous les autres utilisent INPUT. Erreur silencieuse sinon. |
QgsSnappingConfig.SnappingType |
setType() attend ce type précis — pas Qgis.SnappingType ni Qgis.SnappingTypes. |
| NEIGHBORS obligatoire pour fixgeometrygap | La couche NEIGHBORS produite par checkgeometrygap doit être conservée et passée à fixgeometrygap. |
| Tolérance snapgeometries en unités couche | Si la couche est en Lambert 93 (mètres), 0.5 = 50 cm. Ne pas confondre avec la tolérance en pixels du snapping canvas. |
| METHOD 0 pour PLU gaps | "Plus longue bordure en commun" est la méthode la plus cohérente pour les zonages PLU (le gap va à la zone avec laquelle il partage le plus de frontière). |
| AvoidIntersectionsLayers | Mode le plus adapté PLU — évite les overlaps lors de la saisie mais uniquement sur les couches cochées dans les paramètres du projet. |
Statut
- Validé le 2026-06-21 : API snapping,
checkvalidity,checkgeometryoverlap,checkgeometrygaptestés sur QGIS 4.0.3 via MCP Claude Code. - À tester :
fixgeometryoverlap,fixgeometrygap,snapgeometriessur une vraie couche PLU avec erreurs.