From 021747ae06af166d6f367cc0205ba6648b7f69ba Mon Sep 17 00:00:00 2001 From: daniel Date: Wed, 22 Jul 2026 11:12:43 +0200 Subject: [PATCH] wfs-abruf-fix --- modules/Datenabruf.py | 67 ++++++++++++++++++++++++++++++++++-------- modules/LayerLoader.py | 28 +++++++++++++++++- 2 files changed, 82 insertions(+), 13 deletions(-) diff --git a/modules/Datenabruf.py b/modules/Datenabruf.py index 45409d4..7a2b47a 100644 --- a/modules/Datenabruf.py +++ b/modules/Datenabruf.py @@ -19,7 +19,7 @@ Designprinzipien from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple -from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse +from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse, quote import json import time @@ -132,6 +132,7 @@ class Datenabruf: # außer bei WMS (WMS bleibt unverändert). use_bbox = (raumfilter != "ohne") and (str(provider).upper() != "WMS") url = self._build_provider_url(link=link, provider=str(provider), bbox_geom=bbox_geom if use_bbox else None) + print(f"[Datenabruf] URL für {thema!r} ({provider}): {url}") # 2b) Fachdaten abrufen features, error_msg = self._fetch_features( @@ -309,8 +310,11 @@ class Datenabruf: if callable(extent_to_bbox): bbox_str = extent_to_bbox(extent_obj) else: - # Fallback: einfache xmin/ymin/xmax/ymax-Extraktion (duck-typing) - if hasattr(extent_obj, "xmin") and callable(getattr(extent_obj, "xmin")): + # Fallback: xmin/ymin/xmax/ymax-Extraktion (duck-typing) + # QgsRectangle verwendet xMinimum()/yMinimum()/xMaximum()/yMaximum() + if hasattr(extent_obj, "xMinimum") and callable(getattr(extent_obj, "xMinimum")): + bbox_str = f"{extent_obj.xMinimum()},{extent_obj.yMinimum()},{extent_obj.xMaximum()},{extent_obj.yMaximum()}" + elif hasattr(extent_obj, "xmin") and callable(getattr(extent_obj, "xmin")): bbox_str = f"{extent_obj.xmin()},{extent_obj.ymin()},{extent_obj.xmax()},{extent_obj.ymax()}" elif isinstance(extent_obj, (tuple, list)) and len(extent_obj) == 4: bbox_str = f"{extent_obj[0]},{extent_obj[1]},{extent_obj[2]},{extent_obj[3]}" @@ -325,12 +329,26 @@ class Datenabruf: if provider_norm == "WFS": query_params.setdefault("service", "WFS") query_params.setdefault("request", "GetFeature") - query_params.setdefault("outputFormat", "application/json") - if bbox_str: - query_params.setdefault("BBOX", bbox_str) + query_params.setdefault("VERSION", "2.0.0") + # WFS 2.0: 'typename' → 'TYPENAMES' + if "typename" in query_params and "TYPENAMES" not in query_params: + query_params["TYPENAMES"] = query_params.pop("typename") + # resultType=hits: nur Anzahl vom Server abfragen, kein GML-Download + query_params.setdefault("resultType", "hits") + # CRS in OGC-URN-Format (wie QGIS Datenquellenverwaltung) + srsname_urn: Optional[str] = None if crs_authid: - query_params.setdefault("SRSNAME", crs_authid) - new_query = urlencode(query_params, doseq=True) + if crs_authid.upper().startswith("EPSG:"): + srsname_urn = f"urn:ogc:def:crs:EPSG::{crs_authid.split(':', 1)[1]}" + else: + srsname_urn = crs_authid + if bbox_str: + # 5. Komponente: CRS-URN anhängen (Standard für WFS 2.0) + bbox_with_crs = f"{bbox_str},{srsname_urn}" if srsname_urn else bbox_str + query_params.setdefault("BBOX", bbox_with_crs) + if srsname_urn: + query_params.setdefault("SRSNAME", srsname_urn) + new_query = urlencode(query_params, doseq=True, quote_via=quote, safe=':/') rebuilt = parsed._replace(query=new_query) return urlunparse(rebuilt) @@ -389,14 +407,14 @@ class Datenabruf: query_params.setdefault("inSR", srid) query_params.setdefault("outSR", srid) - new_query = urlencode(query_params, doseq=True) + new_query = urlencode(query_params, doseq=True, quote_via=quote, safe=':/') rebuilt = parsed_rest._replace(query=new_query) return urlunparse(rebuilt) # Default: generischer bbox-Parameter (nur wenn vorhanden) if bbox_str: query_params.setdefault("bbox", bbox_str) - new_query = urlencode(query_params, doseq=True) + new_query = urlencode(query_params, doseq=True, quote_via=quote, safe=':/') rebuilt = parsed._replace(query=new_query) return urlunparse(rebuilt) @@ -573,7 +591,33 @@ class Datenabruf: # Sonst: gib das gesamte JSON als einzelnes Objekt zurück return [parsed], None except json.JSONDecodeError: - # Nicht-JSON-Antwort (z. B. GML). Wenn QGIS verfügbar, versuche GML via temporärer Datei + OGR + # Nicht-JSON-Antwort (GML/XML). Für WFS resultType=hits: Anzahl aus XML lesen. + if prov == "WFS": + try: + import xml.etree.ElementTree as ET + root = ET.fromstring(response_text) + # WFS 2.0: numberOfMatched; WFS 1.x: numberOfFeatures + n_str = ( + root.get("numberOfMatched") + or root.get("numberMatched") + or root.get("numberOfFeatures") + or root.get("numberOfReturned") + ) + if n_str is not None: + try: + count = int(n_str) + return [{}] * count, None + except (ValueError, TypeError): + pass + # Fallback: wfs:member-Elemente zählen + members = [ + c for c in root + if c.tag.lower().endswith(("member", "featuremember")) + ] + return [{}] * len(members), None + except Exception as exc: + return [], f"WFS-XML-Antwort konnte nicht geparst werden: {exc}" + # Nicht-WFS, nicht-JSON: GML via temporärer Datei + OGR if getattr(qgiscore, "QGIS_AVAILABLE", False): try: import tempfile @@ -588,5 +632,4 @@ class Datenabruf: return [], "GML-Antwort konnte nicht als Layer geladen werden" except Exception as exc: return [], f"Fehler beim Parsen von GML: {exc}" - # Wenn alles fehlschlägt: return [], "Antwort konnte nicht als JSON oder GML geparst werden" diff --git a/modules/LayerLoader.py b/modules/LayerLoader.py index f609fa3..363aaa4 100644 --- a/modules/LayerLoader.py +++ b/modules/LayerLoader.py @@ -122,7 +122,33 @@ class LayerLoader: try: if provider_lower == "wfs": - uri = link if link.strip().lower().startswith("url=") else f"url={link}" + raw_link = link.strip() + if raw_link.lower().startswith("url="): + raw_link = raw_link[4:] + # QGIS WFS-Provider erwartet die Basis-URL und 'typename' als + # separaten URI-Parameter. GetFeature-spezifische Parameter + # (request, outputFormat, resultType, BBOX, …) werden entfernt. + try: + from urllib.parse import urlparse as _up, parse_qsl as _pqsl, urlunparse as _uu, urlencode as _ue + _parsed = _up(raw_link) + _params = dict(_pqsl(_parsed.query, keep_blank_values=True)) + _typename = ( + _params.pop("typename", None) + or _params.pop("TYPENAME", None) + or _params.pop("TYPENAMES", None) + ) + for _k in ("request", "REQUEST", "outputFormat", "OUTPUTFORMAT", + "resultType", "RESULTTYPE", "BBOX", "bbox", + "STARTINDEX", "COUNT"): + _params.pop(_k, None) + _params.setdefault("service", "WFS") + _params.setdefault("version", "2.0.0") + _base_url = _uu(_parsed._replace(query=_ue(_params))) + uri = f"url={_base_url}" + if _typename: + uri += f" typename={_typename}" + except Exception: + uri = link if link.strip().lower().startswith("url=") else f"url={link}" layer = QgsVectorLayer(uri, thema, "WFS") elif provider_lower == "wms": uri = link if link.strip().lower().startswith("url=") else f"url={link}"