Files
Plugin_SN_Verfahrensgebiet/functions/flurstueck_suche.py
T

176 lines
6.3 KiB
Python
Raw Normal View History

"""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))