Files
erik 091ef281b0 QGIS-Plugin VLN Karten: Verfahrens-Layer der Karten-API laden und hochladen
Lädt Verfahrensgebiet, Plan 41, Karte alter Stand (KAS) und Wertermittlung
(WE) je VKZ vollständig aus KARTE_OBJEKT (Listen-Endpunkt mit Paging) und
schreibt sie per PUT zurück. Einmaliger Login (mail/password -> userauth),
API-Key persistiert in QSettings; Verfahrens-Auswahl in der Toolbar wird
je QGIS-Projekt gemerkt. Gemischte Geometrietypen werden beim Laden in
Punkte-/Linien-/Flächen-Layer gesplittet und beim Hochladen wieder
vereint. Qt5/Qt6-kompatibel (QGIS 3.22+ und QGIS 4).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:31:44 +02:00

179 lines
6.4 KiB
Python

"""Konvertierung GeoJSON <-> QGIS-Layer.
Die API liefert je Layer (z.B. Plan 41) gemischte Geometrietypen in einer
FeatureCollection: Punkte, Linien, Polygone sowie deren Multi-Varianten.
QGIS-Memory-Layer können nur einen Geometrietyp halten — beim Laden wird
deshalb nach Geometrie-Familie (Punkte/Linien/Flächen) in bis zu drei
Layer gesplittet; Single-Geometrien werden zu Multi befördert.
Beim Hochladen müssen alle Teil-Layer desselben Datensatzes wieder zu
EINER FeatureCollection vereint werden, weil der PUT der API den
kompletten Layer-Bestand der VKZ ersetzt (siehe layers_to_feature_collection
und plugin_layers).
Welcher Datensatz/welche VKZ zu einem Layer gehört, steht als
Custom-Property am Layer.
"""
import json
from qgis.core import (
QgsJsonExporter,
QgsJsonUtils,
QgsProject,
QgsVectorLayer,
)
try: # QGIS >= 3.30 und QGIS 4
from qgis.core import Qgis
_GEOM_POINT = Qgis.GeometryType.Point
_GEOM_LINE = Qgis.GeometryType.Line
_GEOM_POLYGON = Qgis.GeometryType.Polygon
except (ImportError, AttributeError): # ältere QGIS-3-Versionen
from qgis.core import QgsWkbTypes
_GEOM_POINT = QgsWkbTypes.PointGeometry
_GEOM_LINE = QgsWkbTypes.LineGeometry
_GEOM_POLYGON = QgsWkbTypes.PolygonGeometry
# Die VLN-API liefert Koordinaten unverändert aus der DB (ST_AsGeoJSON):
# ETRS89 / UTM Zone 33 — verifiziert per GET /maps/umringe (Werte wie
# [406924, 5658760] liegen im Raum Dresden, nicht in Grad).
GEOJSON_CRS = "EPSG:25833"
PROP_DATASET = "vln_karten/dataset"
PROP_VERFAHREN = "vln_karten/verfahren"
PROP_PATH = "vln_karten/api_path"
# Geometrie-Familie -> (Memory-Provider-Typ, Namenszusatz).
# Reihenfolge = Lade-Reihenfolge: Flächen zuerst, damit Punkte im
# Layerbaum oben landen und nicht verdeckt werden.
_FAMILIES = (
("polygon", "MultiPolygon", "Flächen"),
("line", "MultiLineString", "Linien"),
("point", "MultiPoint", "Punkte"),
("none", "None", "ohne Geometrie"),
)
def _family(feature):
"""Geometrie-Familie eines Features. Unbekannte Typen (z.B.
GeometryCollection) landen bei 'none' — Attribute bleiben erhalten,
die Geometrie ginge beim Hochladen verloren."""
if not feature.hasGeometry() or feature.geometry().isNull():
return "none"
gtype = feature.geometry().type()
if gtype == _GEOM_POINT:
return "point"
if gtype == _GEOM_LINE:
return "line"
if gtype == _GEOM_POLYGON:
return "polygon"
return "none"
def memory_layer_from_features(
uri_type, name, fields, features, dataset_key, verfahren_nr, api_path, promote=True
):
"""Baut einen Memory-Layer aus fertigen QgsFeatures, setzt die
Plugin-Custom-Properties und hängt ihn ins Projekt."""
uri = uri_type if uri_type == "None" else "%s?crs=%s" % (uri_type, GEOJSON_CRS)
layer = QgsVectorLayer(uri, name, "memory")
if not layer.isValid():
raise RuntimeError("Memory-Layer '%s' konnte nicht erzeugt werden." % name)
provider = layer.dataProvider()
provider.addAttributes(fields.toList())
layer.updateFields()
prepared = []
for feature in features:
geometry = feature.geometry()
if promote and not geometry.isNull() and not geometry.isMultipart():
geometry.convertToMultiType()
feature.setGeometry(geometry)
prepared.append(feature)
provider.addFeatures(prepared)
layer.updateExtents()
layer.setCustomProperty(PROP_DATASET, dataset_key)
layer.setCustomProperty(PROP_VERFAHREN, verfahren_nr)
layer.setCustomProperty(PROP_PATH, api_path)
return layer
def feature_collection_to_layers(
feature_collection, base_name, default_geometry, dataset_key, verfahren_nr, api_path
):
"""Erzeugt aus einer GeoJSON FeatureCollection je vorkommender
Geometrie-Familie einen Memory-Layer und hängt sie ins Projekt.
Liefert die Liste der erzeugten Layer."""
text = json.dumps(feature_collection)
fields = QgsJsonUtils.stringToFields(text)
features = QgsJsonUtils.stringToFeatureList(text, fields)
groups = {}
for feature in features:
groups.setdefault(_family(feature), []).append(feature)
layers = []
multiple = len(groups) > 1
def make_layer(uri_type, name, group_features, promote):
layers.append(
memory_layer_from_features(
uri_type, name, fields, group_features,
dataset_key, verfahren_nr, api_path, promote=promote,
)
)
if not groups:
# Leerer Datensatz: ein leerer Layer im Default-Typ, damit der
# Nutzer digitalisieren und hochladen kann.
make_layer(default_geometry, base_name, [], promote=False)
else:
for family, uri_type, suffix in _FAMILIES:
if family not in groups:
continue
name = "%s%s" % (base_name, suffix) if multiple else base_name
make_layer(uri_type, name, groups[family], promote=(family != "none"))
QgsProject.instance().addMapLayers(layers)
return layers
def plugin_layers(dataset_key, verfahren_nr):
"""Alle Layer im Projekt, die zu diesem Datensatz und dieser VKZ
gehören (z.B. die Punkte-/Linien-/Flächen-Teil-Layer von Plan 41)."""
result = []
for layer in QgsProject.instance().mapLayers().values():
if (
isinstance(layer, QgsVectorLayer)
and layer.customProperty(PROP_DATASET) == dataset_key
and str(layer.customProperty(PROP_VERFAHREN)) == str(verfahren_nr)
):
result.append(layer)
return result
def layers_to_feature_collection(layers):
"""Vereint die Features mehrerer Layer zu EINER GeoJSON
FeatureCollection (für den PUT, der den Serverbestand ersetzt).
Keine Transformation nach WGS84: Die API erwartet die Koordinaten im
selben CRS, in dem sie sie liefert (EPSG:25833)."""
merged = []
for layer in layers:
exporter = QgsJsonExporter(layer)
exporter.setSourceCrs(layer.crs())
exporter.setTransformGeometries(False)
collection = json.loads(
exporter.exportFeatures(list(layer.getFeatures()))
)
merged.extend(collection.get("features", []))
return {"type": "FeatureCollection", "features": merged}
def layer_api_path(layer):
"""API-Pfad, unter dem dieser Layer geladen wurde — oder None,
wenn der Layer nicht von diesem Plugin stammt."""
if not isinstance(layer, QgsVectorLayer):
return None
return layer.customProperty(PROP_PATH) or None