"""sn_verfahrensgebiet/functions/flurstueck_suche.py – Flurstückssuche im Layer „Flurstücke“ (#56). Findet ein Flurstück über Gemarkung und Flurstücksnummer im Layer „Flurstücke“, den der Bearbeitungs-Tab aus ALKIS lädt. Verglichen wird das ``flurstueckskennzeichen``, nicht die von QGIS abgeflachten Einzelfelder des WFS – deren Namen hängen vom Provider ab, das Kennzeichen nicht. Aufbau des Kennzeichens (20 Zeichen, unbesetzte Teile füllt ALKIS mit ``_``):: 14 0268 ___ 00562 ____ 00 │ │ │ │ │ └ Flurstücksfolge │ │ │ │ └ Nenner │ │ │ └ Zähler │ │ └ Flur (in Sachsen unbesetzt) │ └ Gemarkungsnummer └ Land Die Gemarkung darf als Nummer oder als Name eingegeben werden. Namen werden über den Layer „Gemarkungen“ (vereinfachter ALKIS-Dienst, Feld ``ave_name``) in Nummern übersetzt. Das Modul zeigt keine Dialoge; Fehler kommen als :class:`FlurstueckSucheFehler` mit einer Meldung für den Nutzer zurück. """ from __future__ import annotations import re from dataclasses import dataclass, field from typing import List, Optional, Tuple from sn_basis.functions import QgsProject from sn_verfahrensgebiet.functions.flurstueck_alkis import ( FELD_FLURSTUECKSKENNZEICHEN, FLST_LAYER_NAME, ) from sn_verfahrensgebiet.functions.gemarkung_alkis import _extract_schluessel_und_name GEMARKUNGEN_LAYER_NAME = "Gemarkungen" MELDUNG_KEIN_FLURSTUECKSLAYER = ( "Zuerst Verfahrensgebiet und zugehörige Flurstücke aus ALKIS laden." ) _NUMMER_MUSTER = re.compile(r"^\s*(\d+)\s*(?:/\s*([0-9A-Za-z]+)\s*)?$") class FlurstueckSucheFehler(Exception): """Eingabe- oder Suchfehler; der Text ist für den Nutzer bestimmt.""" class KeinFlurstueckslayer(FlurstueckSucheFehler): """Im Projekt gibt es keinen Layer „Flurstücke“.""" def __init__(self): super().__init__(MELDUNG_KEIN_FLURSTUECKSLAYER) @dataclass class SuchErgebnis: """Treffer einer Flurstückssuche im Layer ``layer``.""" layer: object feature_ids: List[int] = field(default_factory=list) def _normiere_nenner(wert: str) -> str: return (wert or "").strip().strip("_").lstrip("0").lower() def zerlege_flurstuecksnummer(text: str) -> Tuple[str, str]: """Zerlegt ``"562"`` in ``("562", "")`` und ``"12/3"`` in ``("12", "3")``. Führende Nullen fallen weg, damit ``"0562"`` und ``"562"`` gleich sind. """ treffer = _NUMMER_MUSTER.match(text or "") if not treffer: raise FlurstueckSucheFehler( "Die Flurstücksnummer bitte als Zahl oder als Zähler/Nenner eingeben, " "z. B. 562 oder 12/3." ) zaehler = treffer.group(1).lstrip("0") or "0" return zaehler, _normiere_nenner(treffer.group(2) or "") def zerlege_kennzeichen(kennzeichen) -> Optional[Tuple[str, str, str]]: """Liefert ``(gemarkungsnummer, zaehler, nenner)`` eines Flurstückskennzeichens. ``None``, wenn das Kennzeichen zu kurz ist oder keine Gemarkung bzw. keinen Zähler enthält. """ kz = str(kennzeichen or "").strip() if len(kz) < 18: return None gemarkung = kz[2:6] zaehler = kz[9:14].strip("_").lstrip("0") if not gemarkung.isdigit() or not zaehler: return None return gemarkung, zaehler, _normiere_nenner(kz[14:18]) def gemarkungsnummern(eingabe: str, gemarkungen_layer=None) -> List[str]: """Übersetzt die Gemarkungseingabe in 4-stellige Gemarkungsnummern. Ziffern gelten als Nummer: bis zu vier Stellen werden mit Nullen aufgefüllt, bei längeren Schlüsseln (z. B. ``140268``) zählen die letzten vier Stellen. Ein Name wird ohne Rücksicht auf Groß- und Kleinschreibung im Layer „Gemarkungen“ gesucht. Gleichnamige Gemarkungen liefern mehrere Nummern. """ text = (eingabe or "").strip() if not text: raise FlurstueckSucheFehler("Bitte eine Gemarkung eingeben (Name oder Nummer).") if text.isdigit(): return [text[-4:].zfill(4)] if gemarkungen_layer is None: raise FlurstueckSucheFehler( f"Den Gemarkungsnamen „{text}“ kann das Plugin nur mit dem Layer " f"„{GEMARKUNGEN_LAYER_NAME}“ auflösen. Bitte die Gemarkungsnummer " "eingeben oder die Gemarkungen laden." ) gesucht = text.casefold() nummern = set() for feature in gemarkungen_layer.getFeatures(): nummer, name = _extract_schluessel_und_name(feature) if nummer and name.casefold() == gesucht: nummern.add(nummer.zfill(4)) if not nummern: raise FlurstueckSucheFehler( f"Im Layer „{GEMARKUNGEN_LAYER_NAME}“ gibt es keine Gemarkung „{text}“." ) return sorted(nummern) def finde_flurstuecke(layer, nummern: List[str], zaehler: str, nenner: str) -> List[int]: """Feature-IDs der Flurstücke mit passender Gemarkung, Zähler und Nenner. Ohne Nenner passen alle Flurstücke mit diesem Zähler (``562``, ``562/1`` …). """ if layer.fields().indexFromName(FELD_FLURSTUECKSKENNZEICHEN) < 0: raise FlurstueckSucheFehler( f"Der Layer „{FLST_LAYER_NAME}“ hat kein Feld „{FELD_FLURSTUECKSKENNZEICHEN}“." ) gesucht = set(nummern) ids: List[int] = [] for feature in layer.getFeatures(): teile = zerlege_kennzeichen(feature[FELD_FLURSTUECKSKENNZEICHEN]) if teile is None: continue gemarkung, f_zaehler, f_nenner = teile if gemarkung in gesucht and f_zaehler == zaehler and (not nenner or f_nenner == nenner): ids.append(feature.id()) return ids def _hole_layer(name: str): treffer = QgsProject.instance().mapLayersByName(name) return treffer[0] if treffer else None def flurstueck_suchen(gemarkung_text: str, nummer_text: str) -> SuchErgebnis: """Sucht das Flurstück im Layer „Flurstücke“ des aktuellen Projekts. Raises ------ KeinFlurstueckslayer Der Layer „Flurstücke“ fehlt. FlurstueckSucheFehler Die Eingabe ist unvollständig oder ungültig. """ flst_layer = _hole_layer(FLST_LAYER_NAME) if flst_layer is None: raise KeinFlurstueckslayer() zaehler, nenner = zerlege_flurstuecksnummer(nummer_text) nummern = gemarkungsnummern(gemarkung_text, _hole_layer(GEMARKUNGEN_LAYER_NAME)) return SuchErgebnis(flst_layer, finde_flurstuecke(flst_layer, nummern, zaehler, nenner))