Community • 16. August 2023

Postprocessing der Ergebnisse von QGIS-Verarbeitungswerkzeugen

QGIS bietet mit der grafischen Modellierung zwar ein einfach zu bedienendes Tool, welches es ermöglicht, sich mittels Verknüpfung existierender Algorithmen ganz individuelle, eigene Verarbeitungswerkzeuge zu erstellen - bei komplexeren Abläufen stößt man aber oft an dessen Grenzen. Besonders was die anschließende Repräsentation und Organisation der berechneten oder analysierten Daten angeht, bleiben in der grafischen Modellierung gerne einige Wünsche offen.

Implementiert man die neuen Werkzeuge stattdessen mit Python-Code, so ergeben sich weitere Möglichkeiten. Auch ohne ein Plugin programmieren zu müssen, können zum Beispiel die Ergebnislayer eines Verarbeitungswerkzeugs dynamisch umbenannt, gestylt oder gruppiert werden. Dies soll in diesem Artikel beispielhaft demonstriert und erläutert werden.

Verarbeitungswerkzeuge in Python

Die PyQGIS-Klasse QgsProcessingAlgorithm kann als Vorlage verwendet werden, um eigene Verarbeitungswerkzeuge zu programmieren. Dies kann entweder über den Export eines Modells als Python-Skript beginnen oder "from scratch" durch manuelle Implementation der benötigten Methoden der abstrakten Klasse. Neben einigen weniger spannenden Methoden, welche QGIS Informationen über den neuen Algorithmus liefern sollen (etwa den Namen oder die Dokumentation zur Anzeige in der GUI) gibt es dazu die Funktionen initAlgorithm() und processAlgorithm():

  • In initAlgorithm() werden Ein- und Ausgangsparameter des Werkzeugs definiert, etwa dass es Features eines Vektorlayers laden und einen Rasterlayer ausgeben kann. QGIS nutzt diese Informationen zum Beispiel zur automatischen Erstellung eines entsprechenden Dialogs, wenn man den Algorithmus später über die Verarbeitungswerkzeugleiste aufruft, oder für die korrekte Verknüpfbarkeit zwischen Algorithmen im Modellentwurf.

  • In processAlgorithm() wird der eigentliche Algorithmus implementiert. Hier sind der Fantasie praktisch keine Grenzen gesetzt. Es können bestehende Algorithmen verwendet oder auch ein völlig neuer Code auf Basis der PyQGIS-API oder jeglicher anderer Python-Module geschrieben werden. Was aber gerne zu Problemen führt, sind Interaktionen des Codes mit der GUI einer laufenden QGIS-Instanz. Dynamische Benennungen oder individuelle Gruppierungen von Layern, welche nach Beendigung eines Algorithmus im Projekt geladen werden, sind typische Beispielfälle, die sich über "Postprocessing" lösen lassen.

Postprocessing

QGIS bietet zwei Möglichkeiten, um nach Beendigung eines Algorithmus noch einen weiteren, zum Algorithmus gehörigen Code auszuführen: Die Methode QgsProcessingAlgorithm.postProcessAlgorithm() und die Klasse QgsProcessingLayerPostprozessorInterface. Während erstere zum Beispiel ermöglicht temporäre Dateien zu entfernen oder etwas besonderes mit allen Ergebnisdaten eines Algorithmus durchzuführen, dient letztes zum Prozessieren einzelner Ergebnislayer. Im Folgenden implementieren wir in einem kleinen Beispiel einen layer-spezifischen Postprozessor, welcher nach Beendigung des Algorithmus ausgewählte Ergebnislayer dynamisch umbenennt, einen Stil zuweist und in eine Layergruppe einsortiert.

Der Plan

Als fiktive Aufgabe berechnet unser Algorithmus 3 Puffer um die Eingangsgeometrien. Einen "Haupt"-Pufferlayer mit der von den Nutzenden angegebenen Distanz sowie zwei Pufferlayer in der zwei- bzw. dreifachen Distanz, bei denen die Ergebnisgeometrien zusätzlich verschmolzen werden sollen, falls sie sich überlappen. Die verschmolzenen Puffer sollen nach der Anzahl der verbliebenen Features benannt und eingefärbt werden. Zusätzlich sollen diese Ergebnislayer in eine eigene Layergruppe gesteckt werden.

Der Algorithmus an sich

Die Eingabeparameter des Algorithmus sind eine "QgsProcessingParameterFeatureSource" für die Eingangsgeometrien, eine "QgsProcessingParameterDistance" für die Spezifikation der Pufferdistanz (welche sich für die Definition der Distanzeinheiten auf die FeatureSource bezieht) sowie drei "QgsProcessingParameterVectorDestination"s für die Resultate (schlicht und einfach durchnummeriert als "BUFFER1" bis "BUFFER3".

class BufferRenameStyleGroup(QgsProcessingAlgorithm):
    ...

    def initAlgorithm(self, config=None):
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                name="VECTOR",
                description="Vector layer",
                types=[QgsProcessing.TypeVectorAnyGeometry],
            )
        )

        self.addParameter(
            QgsProcessingParameterDistance(
                name="INITIAL_BUFFER_DISTANCE",
                description="Initial buffer distance",
                parentParameterName="VECTOR",  # for units
                defaultValue=1,
            )
        )

        self.addParameter(
            QgsProcessingParameterVectorDestination(
                name="BUFFER1",
                description="Buffer #1",
            )
        )
        ...
    ...

Der Algorithmus an sich ist wenig aufregend: Dreimal wird der native:buffer-Algorithmus aufgerufen, mit unterschiedlicher Distanz und bei den beiden größeren Puffern mit den Optionen "DISSOLVE": True (-> Geometrien verschmelzen) und "SEPARATE_DISJOINT": True (-> räumlich getrennte Geometrien bei der Verschmelzung als eigenständige Geometrien behalten, diese Option ist seit QGIS 3.32 verfügbar). Der Einfachheit halber sind diese ebenfalls einfach durchnummeriert.

class BufferRenameStyleGroup(QgsProcessingAlgorithm):
    ...

    def processAlgorithm(self, parameters, context, feedback):
        native_buffer1 = processing.run(...)
        native_buffer2 = processing.run(...)
        native_buffer3 = processing.run(
            "native:buffer",
            {
                "INPUT": parameters["VECTOR"],
                "DISTANCE": parameters["INITIAL_BUFFER_DISTANCE"] * 3,
                "DISSOLVE": True,
                "SEPARATE_DISJOINT": True,  # available since QGIS 3.32
                "OUTPUT": parameters["BUFFER3"],  # for QGIS to load
            },
            ...
        )
        ...
    ...

Am Ende haben wir 3 Layer, welche als Ergebnisse von QGIS geladen werden: Den "Haupt"-Pufferlayer sowie die beiden verschmolzenen größeren Pufferlayer.

Der Postprozessor

Rein funktionell ist der Algorithmus hier fertig, aber für die Benutzerfreundlichkeit steht noch das Umbenennen, Stylen und Gruppieren aus. Perfekte Aufgaben für einen Postprozessor! QGIS stellt dazu ein Interface QgsProcessingLayerPostprozessorInterface bereit, welches genutzt werden kann, um beliebige eigene Postprozessoren zu implementieren. Unser Postprozessor könnte zum Beispiel folgendermaßen aufgebaut sein:

class LayerPostprozessor(QgsProcessingLayerPostprozessorInterface):

    def __init__(self, name_template: str):
        super().__init__()
        self.name_template = name_template

    def postProcessLayer(self, layer, context, feedback):
        feedback.pushInfo(f"Post-processing for layer {layer.name()!r}:")

        number_of_features = layer.featureCount()

        layer_name = self.name_template.format(n=number_of_features)
        feedback.pushInfo(f"Renaming to {layer_name!r}")
        layer.setName(layer_name)

        if number_of_features > 50:
            feedback.pushInfo("Assigning style... Red!")
            layer.loadNamedStyle("/tmp/red.qml")
        else:
            feedback.pushInfo("Assigning style... Blue!")
            layer.loadNamedStyle("/tmp/blue.qml")

        feedback.pushInfo("Moving into layer group...")
        root = context.project().layerTreeRoot()
        layer_tree_layer = root.findLayer(layer.id())
        clone = layer_tree_layer.clone()
        parent = layer_tree_layer.parent()
        group = root.findGroup("Abgezählte Puffer")
        if not group:
            group = root.insertGroup(0, "Abgezählte Puffer")
        group.insertChildNode(0, clone)
        parent.removeChildNode(layer_tree_layer)

        feedback.pushInfo(f"Post-processing for layer {layer.name()!r}: Done!")

Bei der Instanziierung mit der __init__-Methode nimmt unser Postprozessor ein Template (einen String mit {}-Platzhaltern) für die spätere dynamische Benennung des jeweiligen Layers an.

Die postProcessLayer-Methode wird von QGIS automatisch und mit praktischen Parametern aufgerufen, wenn nach Beendigung des Algorithmus das Postprocessing ansteht.

Sowohl für die Benennung als auch für die Auswahl des Stils wird die Anzahl der Features des Layers benötigt, diese wird direkt mit layer.featureCount() abgefragt und anschließend im Namenstemplate eingesetzt. Aus "ABC {n}" wird dann z. B. "ABC 42". Sind mehr als 50 Features im Layer, wird ein anderer QML-Layerstil geladen als sonst. Im Beispiel eine rote, statt einer blauen Polygonfüllung. Und da in dem Moment des Postprocessings QGIS von sich aus auch schon den Ebenenbaum mit den Ergebnislayern bestückt hat, können wir diese dort nach Belieben verschieben, gruppieren u. ä..

Zuweisung des Postprozessors

Um nun einen solchen Postprozessor nach Beendigung eines QgsProcessingAlgorithm einzusetzen, muss er den entsprechenden Layern zugewiesen werden. Das ist leider komplizierter als gedacht, denn hier stoßen wir an die Schnittstellen zwischen QGIS' Python-API und dem C++-Code, der tatsächlich im Hintergrund läuft: Um zu vermeiden, dass unser Postprozessor-Objekt nach Beendigung des Algorithmus automatisch "mitweggeräumt" wird, müssen wir dafür sorgen, dass eine Referenz auf das Objekt bestehen bleibt. Im Beispiel nutzen wir dazu das global-Statement. Andernfalls würde das Postprocessing still und ohne Fehlermeldung wegfallen...

Wir erstellen jeweils einen eigenen Postprozessor für die beiden Layer, da die Namen eine Nummerierung erhalten sollen und speichern Verweise auf diese Objekte einfach in einer Liste:

class BufferRenameStyleGroup(QgsProcessingAlgorithm):
    ...
    def processAlgorithm(self, parameters, context, feedback):

        ...

        # Note: We declare the scope for the post processors to be
        # "global" so that they do not vanish when QGIS cleans up
        # after finishing the QgsProcessingAlgorithm - before the
        # post-processing itself is performed.
        # Ref: https://gis.stackexchange.com/a/384996/51035
        global layer_post_processors
        layer_post_processors = []

        for i, layer in enumerate(
            (native_buffer2["OUTPUT"], native_buffer3["OUTPUT"]),
            start=2,
        ):
            layer_renamer = LayerPostprozessor(
                name_template=f"Puffer {i}: Aufgelöst in {{n}} Objekte"
            )
            # We must not add post-processing if layer is not meant to be loaded
            # ref: https://qgis.org/pyqgis/master/core/QgsProcessingContext.html#qgis.core.QgsProcessingContext.layerToLoadOnCompletionDetails
            if context.willLoadLayerOnCompletion(layer):
                layer_details = context.layerToLoadOnCompletionDetails(layer)
                layer_details.setPostprozessor(layer_renamer)
                layer_post_processors.append(layer_renamer)
        ...
    ...

Das gesamte Beispielskript können Sie hier herunterladen: "QgsProcessingAlgorithm_BufferRenameStyleGroup.py"

Damit ist unser Algorithmus fertig definiert und kann ausgeführt werden:

Das Ergebnis haben Sie bereits im Eingangsbild bestaunen können: Die beiden Extralayer liegen in einer eigenen Layergruppe namens "Abgezählte Puffer" und sind dynamisch gemäß der Anzahl der Features eingefärbt und benannt worden.

Fazit

Mit dem erstellten Code (zusätzlich zum eigentlichen Verarbeitungsalgorithmus) wird QGIS nach der Beendigung des Algorithmus ein Postprocessing durchführen und in diesem Fall zwei Layer dynamisch umbenennen, gruppieren und je nach Datenlage mit einem bestimmten Stil versehen. Das mag auf den ersten Blick kompliziert erscheinen, ist aber je nach Anwendungsfall mit wenigen Zeilen Extracode implementiert und erleichtert die Arbeit mit dem Werkzeug anschließend ungemein.

Weiterführende Links und Referenzen

Weitere Beiträge, die Dich interessieren könnten:

Hannes Kroeger

Johannes Kröger

Nach einem Abschluss als Master of Science Geomatik an der HafenCity Universität Hamburg arbeitete Johannes Kröger mehrere Jahre als Wissenschaftlicher Mitarbeiter im dortigen Lab for Geoinformatics and Geovisualization (g2lab) in der Lehre und Forschung. Seit März 2020 ist er als GIS-Entwickler und -berater bei der WhereGroup tätig. Johannes Kröger ist auch privat im Umfeld von Kartografie, Open-Data und FOSSGIS aktiv.

Artikel teilen: