091ef281b0
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>
179 lines
6.4 KiB
Python
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
|