19 October 2014

An der FOSSGIS 2013 wurde von Andreas Schmid gezeigt wie mit QGIS der Basisplan der amtlichen Vermessung erstellt werden kann. Der ganze Systemaufbau ist nicht gerade trivial weil für das Rendern des Kartenbildes QGIS Server eingesetzt wird. Dies setzt z.B. Apache 2 und ein FCGI-Modul voraus. Das eigentliche Ziel der Übung ist aber nicht einen WMS-Dienst anzubieten, sondern aus Vektordaten ein Rasterkartenwerk zu erstellen.

Weil bei Projektstart einige Funktionen in den Python-Bindings von QGIS (PyQGIS) fehlten, konnte nicht dieser Weg gewählt werden, sondern es wurde der Umweg über einen WMS-Dienst als Kartenrenderer gewählt. Die fehlenden Funktionen sind jetzt alle vorhanden und es steht einer Vereinfachung des Herstellungsprozesses nichts mehr im Wege.

Vieles zu PyQGIS steht im Kochbuch. Einige Details fehlen aber. Darum folgt nachstehend ein komplettes Beispiel, das zeigt wie man mit einem vorbereiteten QGIS-Projekt die Rasterkarten anhand einer Blatteinteilung erstellen kann ohne QGIS manuell starten zu müssen.

Das vorbereitete QGIS-Projekt ist simpel. Es besteht nur aus den Gemeindegrenzen des Kantons Solothurn:

QGIS Projekt

Als erstes müssen zwei Dateien erstellt werden:

  • basisplan.sh

  • basisplan.py

basisplan.sh macht nichts weiter als den Pfad zu den QGIS-Bibliotheken zu setzen und anschliessend die Pythondatei basisplan.py aufzurufen:

#!/bin/bash
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/stefan/Apps/qgis_master/lib
export PYTHONPATH=$PYTHONPATH:/home/stefan/Apps/qgis_master/share/qgis/python

python basisplan.py

basisplan.py enthält den interessanteren Teil:

# -*- coding: utf-8 -*-
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from qgis.core import *
from qgis.gui import *

import os
import sys

# Aktuelles Verzeichnis
current_dir = os.path.dirname(os.path.realpath(__file__))

# QGIS initialisieren
app = QApplication(sys.argv)
QgsApplication.setPrefixPath("/home/stefan/Apps/qgis_master", True)
QgsApplication.initQgis()

# QGIS-Projekt laden
QgsProject.instance().setFileName(os.path.join(current_dir,  "bpav5000sw.qgs"))
if not QgsProject.instance().read():
    sys.exit("QGIS-Projekt nicht gefunden.")

# List mit sämtlichen Layer im QGIS-Projekt
lst = []
layerTreeRoot = QgsProject.instance().layerTreeRoot()
for id in layerTreeRoot.findLayerIds():
    node = layerTreeRoot.findLayer(id)
    lst.append(id)

# Layer mit Blatteinteilung laden
layer_name =  "blatteinteilung"
vlayer = QgsVectorLayer(os.path.join(current_dir, "basisplan.gpkg") + "|layername=blatteinteilung", "Blatteinteilung", "ogr")
if not vlayer.isValid():
    sys.exit("Blatteinteilung konnte nicht geladen werden.")

# Rasterkarten erstellen
iter = vlayer.getFeatures()
for feature in iter:
    idx = vlayer.fieldNameIndex('nummer')
    nummer = feature.attributes()[idx].toString()

    # Ausschnitt und Grösse der Karte berechnen
    dpi = 508
    scale = 5000

    geom = feature.geometry()
    p1 = geom.vertexAt(0)
    p2 = geom.vertexAt(2)

    rect = QgsRectangle(p1, p2)

    dx = rect.width()
    dy = rect.height()

    width = (dx/scale) / 0.0254 * dpi
    height = (dy/scale) / 0.0254 * dpi

    # Einstellungen für Kartenrenderer
    mapSettings = QgsMapSettings()
    mapSettings.setMapUnits(0)
    mapSettings.setExtent(rect)
    mapSettings.setOutputDpi(dpi)
    mapSettings.setOutputSize(QSize(width, height))
    mapSettings.setLayers(lst)
    mapSettings.setFlags(QgsMapSettings.DrawLabeling)

    # Karte zeichnen
    img = QImage(QSize(width, height), QImage.Format_Mono)
    img.setDotsPerMeterX(dpi / 25.4 * 1000)
    img.setDotsPerMeterY(dpi / 25.4 * 1000)

    p = QPainter()
    p.begin(img)

    mapRenderer = QgsMapRendererCustomPainterJob(mapSettings, p)
    mapRenderer.start()
    mapRenderer.waitForFinished()

    p.end()

    img.save(os.path.join("/tmp", "bpav" + str(scale) + "_" + str(nummer) + str(".png")), "png")

# Layer mit Blatteinteilung löschen
del vlayer

# QGIS schliessen
QgsApplication.exitQgis()

Zeilen 2 - 8: Module werden geladen.

Zeile 11: Bei mir funktionieren verschiedene QGIS-Methoden nicht korrekt mit relativen Pfaden, wenn diese direkt reingeschrieben werden (z.B. "./bpav5000sw.qgs" für das zu ladende QGIS-Projekt). Wird aber zuerst das aktuelle Verzeichnis (in dem das Skript läuft) ermittelt und dieses für die relativen Pfade verwendet, funktionierts.

Zeilen 14 - 16: Zuerst wird eine Qt-Applikation und anschliessend QGIS initialisiert. Der Pfad in Zeile 15 zeigt auf die lokale QGIS-Installation.

Zeilen 19 - 21: Hier wird das vorbereitete QGIS-Projekt geladen. Falls es nicht gefunden wird, ist der Rückgabewert der Methode QgsProject.instance().read() False.

Zeilen 24 - 28: Für das Rendern der Karte müssen zuerst sämtliche Layer des QGIS-Projekts in eine Liste geschrieben werden. Sollen nur die sichtbaren Layer gerendert werden, kann man diese mit node.isVisible() ermitteln.

Zeilen 31 - 34: Die Blatteinteilung wird als QGIS-Layer zusätzlich geladen.

Zeilen 37ff: Für jedes Feature des vorhin geladenen Layers mit der Blatteinteilung wird nun die Rasterkarte erstellt.

Zeilen 42 - 56: Aufgrund des Massstabes, Auflösung (DPI) und Ausdehnung des Kartenblattes wird die Grösse (Pixelanzahl) und die Boundingbox der zu erstellenden Rasterkarte berechnet.

Zeilen 59 - 65: Die oben ermittelnden Werte werden in einer Settings-Klasse gespeichert, die später der Kartenrenderer verwenden wird. Interessant ist die Zeile 65. Hier können verschiedene Flags angegeben werden. Mit QgsMapSettings.DrawLabeling wird dem Renderer mitgeteilt, dass die Labels gezeichnet werden sollen. Ohne das Flag QgsMapSettings.Antialiasing werden die Karten ohne Antialiasing gezeichnet. Dies ist insbesondere für schwarz-weisse Rasterkarten sinnvoll, da dann 1-Bit-Karten möglich sind. Die Grösse ist um ein vielfaches kleiner als bei RGB-Bildern und die Karte lässt sich mit jeder beliebigen Farbe einfärben.

Mehrere Flags werden mittels Pipe-Zeichen aneinandergereiht, z.B. mit Antialiasing: mapSettings.setFlags(QgsMapSettings.Antialiasing | QgsMapSettings.DrawLabeling).

Zeilen 68 - 81: Als nächstes muss ein QImage-Objekt mit der passenden Grösse, Format und Auflösung erstellt werden. Für unseren Fall eignet sich das Format QImage.Format_Mono bestens, denn es erstellt das gewünschte 1-Bit-Rasterbild. Für Farbbilder eignet sich z.B. QImage.Format_RGB32. Alle möglichen Formate sind hier beschrieben.

Der Kartenrenderer wird auf Zeile 76 gestartet. Die darauffolgende Zeile ist wichtig, da sonst - ohne auf das Ende zu warten - das unfertige Bild physisch auf die Festplatte geschrieben wird.

Nachdem das Bild fertig gerendert ist, wird es an einem gewünschten Ort gespeichert.

Zeile 84: Ohne das explizite Löschen des zusätzlich geladenen Layers verabschiedet sich mein Skript mit einem Segmentation fault.

Zeile 87: QGIS wird geschlossen und alle verwendeten Layer werden gelöscht (siehe Kommentar zu Zeile 84).

Die Renderzeit eines Kartenblattes ist abhängig von der Anzahl der Layer, der Komplexität der Symbologie und der Grösse (Pixelanzahl). Am schnellsten sind schwarz-weisse Karten, die ohne Antialiasing gezeichnet werden. Farbige Karten sind ebenfalls schneller ohne als mit Antialiasing.

Die hier vorgestellte Lösung kann den Herstellungsprozess des Basisplanes der amtlichen Vermessung vereinfachen, da kein WMS-Dienst verwendet wird. Zudem eignet sich diese Lösung wegen der mächtigen Symbologiemöglichkeiten und Flexibilität von QGIS ebenfalls für das Herstellen von Rasterkarten jeglicher Art.

Das QGIS-Beispielprojekt inkl. Shell- und Pythonskript gibt es hier.

Posted by Stefan Ziegler. | QGIS , PyQGIS , Basisplan , Amtliche-Vermessung