Skip to main content

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, checkgeometrygap testés sur QGIS 4.0.3 via MCP Claude Code. À tester : fixgeometryoverlap, fixgeometrygap, snapgeometries sur une vraie couche PLU avec erreurs.