Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f5215bfe3 | ||
|
|
87341a7278 | ||
|
|
e547cb90c5 | ||
|
|
d88efac0ec | ||
|
|
b105894241 | ||
|
|
29719a8a99 | ||
|
|
c9a5b8ee2b | ||
|
|
c8b380b5d3 | ||
|
|
5004a8d5bf | ||
|
|
3bde666d96 | ||
|
|
e0660aba86 | ||
|
|
84e03d208c | ||
|
|
341b9ab1c4 | ||
|
|
f7661a5d25 | ||
|
|
1aea40d028 | ||
|
|
45a8ce37de | ||
|
|
951b9075fe | ||
|
|
c260ccc2d6 | ||
|
|
49d30eea22 | ||
|
|
0ac0757e42 | ||
|
|
e267d51607 | ||
|
|
003ad1957b | ||
|
|
821bb0640e | ||
|
|
04f2a49c13 | ||
|
|
eac3b44470 | ||
|
|
0a906d3ac7 | ||
|
|
39f8209b9d | ||
|
|
3c0e6e5a19 | ||
|
|
c93776d19e |
@@ -1,3 +1,13 @@
|
||||
- Neu: Anzeige für den Eigentümer im Tab „Eigentümer“. „Daten anzeigen“ in der Trefferliste zeigt Name, Anschrift und E-Mail der gewählten Einträge in einem eigenen Fenster mit Rahmen, das der Bearbeiter auf den Bildschirm des Eigentümers schiebt; Größe und Lage gelten bis zum Neustart von QGIS für die nächste Sitzung weiter. Die Sitzung wird von Hand begonnen und beendet; „Anzeige leeren“ entfernt die Daten, ohne das Fenster zu schließen, damit der nächste Eigentümer sie nicht sieht (#56)
|
||||
- Neu: Angezeigte Daten lassen sich mit „Korrigieren …“ ändern. Die Eigentümerdatei bleibt unverändert; jede Änderung wird mit Zeitpunkt, Konto, Personen-ID, altem und neuem Wert an „<Eigentümerdatei>_korrekturen.csv“ angehängt und sofort angezeigt; Pfad und Name der Korrektur-Datei stehen unter den Knöpfen (#56)
|
||||
- Neu: Eigentümersuche im Tab „Eigentümer“. Den Serienbriefexport (CSV) von Hand als Layer laden, als Eigentümerdatei wählen, Name und/oder Vorname eingeben: Die Trefferliste zeigt Name, Anschrift, E-Mail, den Wert von PER_RECHTSPERSON (0, 1, 5 oder 9, noch nicht entschlüsselt) und das Kennzeichen „verstorben“. Gleiche Personen mit gleicher Anschrift werden zusammengefasst, Groß-/Kleinschreibung und Umlautschreibweise spielen keine Rolle (#56)
|
||||
- Intern: Die Auswahltabelle aus „Verfahrensgebiete aus Landkreis laden“ (Suchfeld, „Alle auswählen“, Mehrfachauswahl) ist als gemeinsame Funktion herausgelöst und wird von der Eigentümer-Trefferliste mitgenutzt; der Landkreis-Dialog verhält sich unverändert (#56)
|
||||
- Neu: Tab „Eigentümer“ mit Flurstückssuche. Gemarkung (Name oder Nummer) und Flurstücksnummer (z. B. 562 oder 12/3) eingeben, die Treffer werden im Layer „Flurstücke“ ausgewählt und angezoomt. Ohne Nenner werden alle Flurstücke mit diesem Zähler gefunden. Fehlt der Layer „Flurstücke“, weist ein Dialog darauf hin, zuerst Verfahrensgebiet und Flurstücke aus ALKIS zu laden (#56)
|
||||
- Bugfix: Nach dem Laden des Verfahrensgebiets (ALKIS oder Shape) sprang die Karte an die falsche Stelle, wenn ein anderes Projekt-KBS als EPSG:25833 eingestellt war. Der Zoom rechnet die Ausdehnung jetzt in das KBS der Kartenansicht um (`zoom_auf_layer` aus sn_basis, setzt sn_basis mit Basis #99 voraus) (Basis #99)
|
||||
- Neu: Grenzpunkte aus ALKIS werden nach dem Laden im ALKIS-Stil dargestellt (styles/alkis_grenzpunkte.qml), statt ungestylt zu bleiben (#53)
|
||||
- Intern: Tests von TestFlurstueckAlkis an die seit längerem geänderte Ladelogik angeglichen (Mocks zeigten noch auf die alte Ein-Query-/Zwei-Knopf-Variante), bbox-Dokumentation in flurstueck_alkis.py korrigiert (bbox wird nicht mehr ausgewertet) – keine Verhaltensänderung (Test-Audit #16)
|
||||
- Intern: Der Button "aus gewählten Gemarkungen laden" nutzt jetzt dieselbe Ladelogik wie "Flurstücke laden" (flurstueck_alkis) statt einer eigenen, parallelen Implementierung – konsolidiert drei bisher getrennte Code-Pfade zum Flurstücke-Laden auf einen (#47)
|
||||
- Bugfix: Flurstücke aus ALKIS an bestehenden Layer anhängen überspringt jetzt bereits vorhandene Flurstücke (Abgleich über flurstueckskennzeichen), statt sie doppelt zu laden – betrifft sowohl den Dialog in "Flurstücke laden" als auch den Button "aus gewählten Gemarkungen laden" (#28)
|
||||
- Fortschrittsdialog beim Laden aus ALKIS schließt jetzt zuverlässig bei jedem Abbruch (Bugfix)
|
||||
- Fortschrittsanzeige beim Laden aus ALKIS in zwei Dialoge aufgeteilt (Suche / eigentlicher Ladevorgang), inkl. eigenem Suchschritt
|
||||
- Mehrfachauswahl bei mehreren BauRaumOderBodenordnungsrecht-Treffern zur Verfahrensnummer (gemeinsames Laden/Verschmelzen mehrerer Objekte)
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
"""sn_verfahrensgebiet/functions/eigentuemer_korrektur.py – Korrekturen beim Datenabgleich (#56).
|
||||
|
||||
Der Bearbeiter korrigiert im Plugin Name, Anschrift oder E-Mail eines
|
||||
angezeigten Eigentümers. Die Eigentümerdatei (Serienbriefexport) bleibt
|
||||
unverändert; jede Änderung wird als eigene Zeile an eine Korrektur-CSV
|
||||
angehängt, standardmäßig ``<Eigentümerdatei>_korrekturen.csv`` im selben Ordner.
|
||||
|
||||
Spalten der Korrektur-CSV (UTF-8 mit BOM, Komma, wie der Export):
|
||||
``Zeitpunkt, EIG_KONTO, PER_ID, Feld, Alter Wert, Neuer Wert``. ``Feld`` ist
|
||||
der Spaltenname aus dem Export. Gehören mehrere Konten oder Personen-IDs zu
|
||||
einem Eintrag, stehen sie durch Leerzeichen getrennt in einer Zelle.
|
||||
|
||||
**Datenschutz:** Die Korrektur-CSV enthält Personendaten. Sie liegt neben
|
||||
dem Export und ist genauso zu schützen wie dieser. Das Modul schreibt
|
||||
nichts auf die Konsole und zeigt keine Dialoge.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import os
|
||||
import re
|
||||
from dataclasses import replace
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer, SPALTEN
|
||||
|
||||
#: Korrigierbare Felder: (Schlüssel, Beschriftung). Der Spaltenname kommt aus SPALTEN.
|
||||
FELDER: List[Tuple[str, str]] = [
|
||||
("anrede", "Anrede"),
|
||||
("titel", "Titel"),
|
||||
("vorname", "Vorname"),
|
||||
("nachname", "Name"),
|
||||
("strasse", "Straße"),
|
||||
("plz", "PLZ"),
|
||||
("wohnort", "Wohnort"),
|
||||
("ortsteil", "Ortsteil"),
|
||||
("email", "E-Mail"),
|
||||
]
|
||||
KOPFZEILE = ["Zeitpunkt", "EIG_KONTO", "PER_ID", "Feld", "Alter Wert", "Neuer Wert"]
|
||||
ENDUNG_KORREKTUR = "_korrekturen.csv"
|
||||
|
||||
Aenderung = Tuple[str, str, str] # (Schlüssel, alter Wert, neuer Wert)
|
||||
|
||||
|
||||
def feldwerte(eigentuemer: Eigentuemer) -> Dict[str, str]:
|
||||
"""Die korrigierbaren Werte eines Eintrags; mehrere E-Mails durch ``, `` getrennt."""
|
||||
werte = {schluessel: getattr(eigentuemer, schluessel, "") for schluessel, _ in FELDER if schluessel != "email"}
|
||||
werte["email"] = ", ".join(eigentuemer.emails)
|
||||
return werte
|
||||
|
||||
|
||||
def ermittle_aenderungen(eigentuemer: Eigentuemer, neue_werte: Dict[str, str]) -> List[Aenderung]:
|
||||
"""Vergleicht die Eingaben mit dem Eintrag; Leerraum am Rand und doppelte Leerzeichen zählen nicht."""
|
||||
alt = feldwerte(eigentuemer)
|
||||
aenderungen: List[Aenderung] = []
|
||||
for schluessel, _ in FELDER:
|
||||
if schluessel not in neue_werte:
|
||||
continue
|
||||
neu = " ".join(str(neue_werte[schluessel] or "").split())
|
||||
if neu != alt[schluessel]:
|
||||
aenderungen.append((schluessel, alt[schluessel], neu))
|
||||
return aenderungen
|
||||
|
||||
|
||||
def wende_an(eigentuemer: Eigentuemer, aenderungen: List[Aenderung]) -> Eigentuemer:
|
||||
"""Liefert den Eintrag mit den neuen Werten (für die Anzeige)."""
|
||||
felder = {}
|
||||
for schluessel, _alt, neu in aenderungen:
|
||||
if schluessel == "email":
|
||||
felder["emails"] = tuple(t.strip() for t in neu.split(",") if t.strip())
|
||||
else:
|
||||
felder[schluessel] = neu
|
||||
return replace(eigentuemer, **felder)
|
||||
|
||||
|
||||
def korrektur_pfad(layer_quelle: str) -> Optional[str]:
|
||||
"""Vorgeschlagene Korrektur-CSV neben der Eigentümerdatei, oder ``None``.
|
||||
|
||||
Versteht die Quellangaben des Textdatei-Providers
|
||||
(``file:///D:/ordner/export.csv?type=csv&…``) und des OGR-Providers
|
||||
(``D:/ordner/export.csv|layername=export``).
|
||||
"""
|
||||
quelle = (layer_quelle or "").strip()
|
||||
if not quelle:
|
||||
return None
|
||||
if quelle.lower().startswith("file:"):
|
||||
pfad = unquote(urlparse(quelle).path)
|
||||
if re.match(r"^/[A-Za-z]:", pfad):
|
||||
pfad = pfad[1:]
|
||||
else:
|
||||
pfad = quelle.split("|", 1)[0]
|
||||
stamm, endung = os.path.splitext(pfad)
|
||||
if endung.lower() != ".csv":
|
||||
return None
|
||||
return stamm + ENDUNG_KORREKTUR
|
||||
|
||||
|
||||
def schreibe_korrekturen(
|
||||
pfad: str,
|
||||
eigentuemer: Eigentuemer,
|
||||
aenderungen: List[Aenderung],
|
||||
zeitpunkt: Optional[datetime] = None,
|
||||
) -> int:
|
||||
"""Hängt die Änderungen an die Korrektur-CSV an und gibt die Zahl der Zeilen zurück.
|
||||
|
||||
Legt die Datei mit Kopfzeile an, wenn sie fehlt oder leer ist.
|
||||
|
||||
Raises
|
||||
------
|
||||
OSError
|
||||
Die Datei lässt sich nicht schreiben.
|
||||
"""
|
||||
if not aenderungen:
|
||||
return 0
|
||||
stempel = (zeitpunkt or datetime.now()).strftime("%Y-%m-%d %H:%M:%S")
|
||||
konten = " ".join(eigentuemer.konten)
|
||||
per_ids = " ".join(eigentuemer.per_ids)
|
||||
neu_anlegen = not os.path.exists(pfad) or os.path.getsize(pfad) == 0
|
||||
with open(pfad, "a", encoding="utf-8-sig" if neu_anlegen else "utf-8", newline="") as datei:
|
||||
schreiber = csv.writer(datei, delimiter=",", lineterminator="\r\n")
|
||||
if neu_anlegen:
|
||||
schreiber.writerow(KOPFZEILE)
|
||||
for schluessel, alt, neu in aenderungen:
|
||||
schreiber.writerow([stempel, konten, per_ids, SPALTEN[schluessel], alt, neu])
|
||||
return len(aenderungen)
|
||||
@@ -0,0 +1,229 @@
|
||||
"""sn_verfahrensgebiet/functions/eigentuemer_suche.py – Eigentümersuche in einer CSV (#56).
|
||||
|
||||
Die Eigentümerdaten kommen als CSV-Export (Serienbriefexport, Kopfzeile siehe
|
||||
:data:`SPALTEN`), den der Anwender von Hand als Layer ins Projekt lädt. Das
|
||||
Plugin speichert davon nichts, es liest nur den Layer.
|
||||
|
||||
Gesucht wird über Nachname und Vorname, ohne Rücksicht auf Groß- und
|
||||
Kleinschreibung und Umlautschreibweise (``Müller`` findet ``Mueller``). Ein
|
||||
leeres Feld schränkt nicht ein. Zeilen mit gleichem Namen und gleicher Anschrift
|
||||
werden zu einem :class:`Eigentuemer` zusammengefasst; dieselbe Person mit einer
|
||||
anderen Anschrift bleibt ein eigener Eintrag.
|
||||
|
||||
**Datenschutz:** Das Modul schreibt keine Personendaten in Ausgaben oder
|
||||
Meldungen. Es zeigt keine Dialoge; Fehler kommen als
|
||||
:class:`EigentuemerSucheFehler` mit einer Meldung ohne Personendaten zurück.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from sn_basis.functions import QgsProject
|
||||
|
||||
#: Spalten des Serienbriefexports, die das Plugin auswertet.
|
||||
SPALTEN = {
|
||||
"konto": "EIG_KONTO",
|
||||
"per_id": "PER_ID",
|
||||
"verstorben": "PER_VERSTORBEN",
|
||||
"anrede": "Anrede",
|
||||
"titel": "Titel",
|
||||
"vorname": "Vorname",
|
||||
"nachname": "Nachname",
|
||||
"ortsteil": "OT",
|
||||
"strasse": "STRASSE",
|
||||
"plz": "PLZ",
|
||||
"wohnort": "WOHNORT",
|
||||
"email": "Email",
|
||||
"rechtsperson": "PER_RECHTSPERSON",
|
||||
}
|
||||
PFLICHTSPALTEN = ("Nachname", "Vorname")
|
||||
|
||||
_NEIN_WERTE = {"", "0", "n", "nein", "f", "false", "falsch", "null"}
|
||||
_UMLAUTE = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue"})
|
||||
|
||||
|
||||
class EigentuemerSucheFehler(Exception):
|
||||
"""Eingabe- oder Layerfehler; der Text enthält keine Personendaten."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Eigentuemer:
|
||||
"""Ein Treffer der Eigentümersuche, zusammengefasst über gleiche Zeilen."""
|
||||
|
||||
anrede: str = ""
|
||||
titel: str = ""
|
||||
vorname: str = ""
|
||||
nachname: str = ""
|
||||
strasse: str = ""
|
||||
plz: str = ""
|
||||
wohnort: str = ""
|
||||
ortsteil: str = ""
|
||||
emails: Tuple[str, ...] = ()
|
||||
verstorben: bool = False
|
||||
rechtsperson: Tuple[str, ...] = ()
|
||||
per_ids: Tuple[str, ...] = ()
|
||||
konten: Tuple[str, ...] = ()
|
||||
feature_ids: Tuple[int, ...] = ()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""``Nachname, Titel Vorname`` für Listen."""
|
||||
vorne = " ".join(t for t in (self.titel, self.vorname) if t)
|
||||
return ", ".join(t for t in (self.nachname, vorne) if t)
|
||||
|
||||
@property
|
||||
def anschrift(self) -> str:
|
||||
"""Einzeilige Anschrift, z. B. ``Hauptstraße 1, 01234 Musterstadt, OT Beispiel``."""
|
||||
ort = " ".join(t for t in (self.plz, self.wohnort) if t)
|
||||
ortsteil = self.ortsteil
|
||||
if ortsteil and not ortsteil.upper().startswith("OT"):
|
||||
ortsteil = f"OT {ortsteil}"
|
||||
return ", ".join(t for t in (self.strasse, ort, ortsteil) if t)
|
||||
|
||||
@property
|
||||
def hinweise(self) -> str:
|
||||
"""Kennzeichen für die Trefferliste (nicht für das Anzeigefenster)."""
|
||||
return "verstorben" if self.verstorben else ""
|
||||
|
||||
@property
|
||||
def rechtsperson_text(self) -> str:
|
||||
"""Rohwert(e) von ``PER_RECHTSPERSON`` (bekannt: 0, 1, 5, 9), noch nicht entschlüsselt."""
|
||||
return ", ".join(self.rechtsperson)
|
||||
|
||||
|
||||
def _text(wert) -> str:
|
||||
"""Attributwert als bereinigter Text; QGIS-NULL und ``None`` werden leer."""
|
||||
if wert is None:
|
||||
return ""
|
||||
ist_null = getattr(wert, "isNull", None)
|
||||
if callable(ist_null):
|
||||
try:
|
||||
if ist_null():
|
||||
return ""
|
||||
except Exception:
|
||||
pass
|
||||
text = " ".join(str(wert).split())
|
||||
return "" if text == "NULL" else text
|
||||
|
||||
|
||||
def _plz(text: str) -> str:
|
||||
"""Stellt führende Nullen wieder her.
|
||||
|
||||
QGIS erkennt beim Laden einer CSV Feldtypen und liest ``01067`` dann als
|
||||
Ganzzahl ``1067`` – in Sachsen beginnt fast jede PLZ mit 0.
|
||||
"""
|
||||
if text.endswith(".0") and text[:-2].isdigit():
|
||||
text = text[:-2]
|
||||
return text.zfill(5) if text.isdigit() and len(text) < 5 else text
|
||||
|
||||
|
||||
def _ist_gesetzt(wert) -> bool:
|
||||
return _text(wert).casefold() not in _NEIN_WERTE
|
||||
|
||||
|
||||
def _vergleichstext(text: str) -> str:
|
||||
"""Für die Suche: Groß-/Kleinschreibung, ß und Umlautschreibweise angleichen."""
|
||||
return " ".join(text.split()).casefold().translate(_UMLAUTE)
|
||||
|
||||
|
||||
def _spalten_des_layers(layer) -> Dict[str, str]:
|
||||
"""Ordnet die Spaltennamen des Layers den Schlüsseln aus :data:`SPALTEN` zu.
|
||||
|
||||
Verglichen wird ohne Groß-/Kleinschreibung und ohne Byte-Order-Mark, das
|
||||
beim Laden einer UTF-8-CSV an der ersten Spalte hängen bleiben kann.
|
||||
"""
|
||||
vorhanden = {}
|
||||
for name in layer.fields().names():
|
||||
vorhanden[str(name).replace("", "").strip().casefold()] = name
|
||||
return {
|
||||
schluessel: vorhanden[spalte.casefold()]
|
||||
for schluessel, spalte in SPALTEN.items()
|
||||
if spalte.casefold() in vorhanden
|
||||
}
|
||||
|
||||
|
||||
def ist_eigentuemerlayer(layer) -> bool:
|
||||
"""``True``, wenn der Layer die Spalten ``Nachname`` und ``Vorname`` hat."""
|
||||
try:
|
||||
spalten = _spalten_des_layers(layer)
|
||||
except Exception:
|
||||
return False
|
||||
return "nachname" in spalten and "vorname" in spalten
|
||||
|
||||
|
||||
def eigentuemerlayer_im_projekt() -> List[object]:
|
||||
"""Alle Layer des Projekts, die als Eigentümerdatei taugen, nach Namen sortiert."""
|
||||
layer = [lyr for lyr in QgsProject.instance().mapLayers().values() if ist_eigentuemerlayer(lyr)]
|
||||
return sorted(layer, key=lambda lyr: lyr.name().casefold())
|
||||
|
||||
|
||||
def _gruppenschluessel(werte: Dict[str, str]) -> Tuple[str, ...]:
|
||||
return tuple(werte[k].casefold() for k in ("nachname", "vorname", "strasse", "plz", "wohnort", "ortsteil"))
|
||||
|
||||
|
||||
def suche_eigentuemer(layer, nachname: str, vorname: str) -> List[Eigentuemer]:
|
||||
"""Sucht Eigentümer, deren Nach- und Vorname die Eingaben enthalten.
|
||||
|
||||
Raises
|
||||
------
|
||||
EigentuemerSucheFehler
|
||||
Beide Felder sind leer oder dem Layer fehlen die Pflichtspalten.
|
||||
"""
|
||||
such_nachname = _vergleichstext(nachname or "")
|
||||
such_vorname = _vergleichstext(vorname or "")
|
||||
if not such_nachname and not such_vorname:
|
||||
raise EigentuemerSucheFehler("Bitte einen Namen oder Vornamen eingeben.")
|
||||
|
||||
spalten = _spalten_des_layers(layer)
|
||||
if "nachname" not in spalten or "vorname" not in spalten:
|
||||
raise EigentuemerSucheFehler(
|
||||
f"Der Layer „{layer.name()}“ hat nicht die Spalten "
|
||||
f"{' und '.join(PFLICHTSPALTEN)}. Ist es der Serienbriefexport?"
|
||||
)
|
||||
|
||||
gruppen: Dict[Tuple[str, ...], dict] = {}
|
||||
for feature in layer.getFeatures():
|
||||
werte = {k: _text(feature[spalte]) for k, spalte in spalten.items()}
|
||||
for k in SPALTEN:
|
||||
werte.setdefault(k, "")
|
||||
werte["plz"] = _plz(werte["plz"])
|
||||
if such_nachname and such_nachname not in _vergleichstext(werte["nachname"]):
|
||||
continue
|
||||
if such_vorname and such_vorname not in _vergleichstext(werte["vorname"]):
|
||||
continue
|
||||
|
||||
gruppe = gruppen.setdefault(_gruppenschluessel(werte), {
|
||||
"werte": werte, "emails": [], "per_ids": [], "konten": [], "feature_ids": [],
|
||||
"verstorben": False, "rechtsperson": [],
|
||||
})
|
||||
for k in ("anrede", "titel"):
|
||||
if not gruppe["werte"][k] and werte[k]:
|
||||
gruppe["werte"][k] = werte[k]
|
||||
for liste, wert in (("emails", werte["email"]), ("per_ids", werte["per_id"]),
|
||||
("konten", werte["konto"]), ("rechtsperson", werte["rechtsperson"])):
|
||||
if wert and wert not in gruppe[liste]:
|
||||
gruppe[liste].append(wert)
|
||||
gruppe["feature_ids"].append(feature.id())
|
||||
gruppe["verstorben"] = gruppe["verstorben"] or _ist_gesetzt(werte["verstorben"])
|
||||
|
||||
treffer = []
|
||||
for gruppe in gruppen.values():
|
||||
w = gruppe["werte"]
|
||||
treffer.append(Eigentuemer(
|
||||
anrede=w["anrede"], titel=w["titel"], vorname=w["vorname"], nachname=w["nachname"],
|
||||
strasse=w["strasse"], plz=w["plz"], wohnort=w["wohnort"], ortsteil=w["ortsteil"],
|
||||
emails=tuple(gruppe["emails"]),
|
||||
verstorben=gruppe["verstorben"], rechtsperson=tuple(gruppe["rechtsperson"]),
|
||||
per_ids=tuple(gruppe["per_ids"]), konten=tuple(gruppe["konten"]),
|
||||
feature_ids=tuple(gruppe["feature_ids"]),
|
||||
))
|
||||
return sorted(treffer, key=lambda e: (e.nachname.casefold(), e.vorname.casefold(), e.wohnort.casefold()))
|
||||
|
||||
|
||||
def hole_layer(layer_id: Optional[str]):
|
||||
"""Layer zur ID aus dem Projekt oder ``None``."""
|
||||
if not layer_id:
|
||||
return None
|
||||
return QgsProject.instance().mapLayer(layer_id)
|
||||
@@ -3,8 +3,9 @@
|
||||
Lädt Flurstücke aus dem ALKIS-WFS-Dienst (``adv:AX_Flurstueck``) anhand von
|
||||
Gemarkungsschlüsseln, die der Nutzer interaktiv eingibt.
|
||||
|
||||
Der WFS-Layer wird direkt (nicht als Memory-Layer) ins Projekt geladen,
|
||||
da die einzelnen Features für die spätere Auswahl benötigt werden.
|
||||
Für jede Gemarkungsnummer wird eine eigene WFS-Anfrage gestellt (umgeht das
|
||||
WFS-Feature-Limit einer Gesamtabfrage); alle Ergebnisse werden anschließend in
|
||||
einem gemeinsamen Memory-Layer zusammengeführt.
|
||||
|
||||
**Erweiterung gegenüber dem Original:**
|
||||
|
||||
@@ -16,10 +17,11 @@ da die einzelnen Features für die spätere Auswahl benötigt werden.
|
||||
- ``vorgabe_namen``: Zugehörige Bezeichnungen, die im Dialog als Hinweis
|
||||
angezeigt werden (kein eigenständiges Feld, nur Anzeigetext).
|
||||
|
||||
- ``bbox``: Optionaler BBox-Parameter, der – sofern übergeben – als
|
||||
zusätzlicher serverseitiger Filter in die WFS-URI eingebettet wird. So
|
||||
werden ausschließlich Flurstücke geladen, die den Verfahrensgebiets-Rahmen
|
||||
überlappen.
|
||||
- ``bbox``: Nicht mehr verwendet. Die Flurstück-Geometrie kommt ausschließlich
|
||||
aus dem ALKIS-WFS je Gemarkung; eine BBox filtert höchstens vorgelagert die
|
||||
Gemarkungsauswahl (siehe ``gemarkung_alkis.py``), nie die Flurstücke selbst.
|
||||
Der Parameter bleibt nur aus Gründen der Signatur-Rückwärtskompatibilität
|
||||
erhalten.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -37,6 +39,7 @@ from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
|
||||
ALKIS_SF_URL = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs"
|
||||
FLST_TYPENAME = "adv:AX_Flurstueck"
|
||||
FLST_LAYER_NAME = "Flurstücke"
|
||||
FELD_FLURSTUECKSKENNZEICHEN = "flurstueckskennzeichen"
|
||||
|
||||
|
||||
def _check_existing_flurstueck_layer(tab_widget):
|
||||
@@ -74,6 +77,24 @@ def _check_existing_flurstueck_layer(tab_widget):
|
||||
return None, LoadStatus.NONE
|
||||
|
||||
|
||||
def _vorhandene_kennzeichen(layer) -> set:
|
||||
"""Liefert die ``flurstueckskennzeichen``-Werte, die ``layer`` bereits enthält.
|
||||
|
||||
Fehlt das Feld, wird eine leere Menge zurückgegeben (kein Abgleich möglich).
|
||||
"""
|
||||
kennzeichen: set = set()
|
||||
if layer.fields().indexFromName(FELD_FLURSTUECKSKENNZEICHEN) < 0:
|
||||
return kennzeichen
|
||||
for feat in layer.getFeatures():
|
||||
try:
|
||||
kz = str(feat[FELD_FLURSTUECKSKENNZEICHEN] or "").strip()
|
||||
except Exception:
|
||||
kz = ""
|
||||
if kz:
|
||||
kennzeichen.add(kz)
|
||||
return kennzeichen
|
||||
|
||||
|
||||
def lade_flurstuecke_per_gemarkung(schluessel_liste: List[str]):
|
||||
"""Lädt Flurstücke für mehrere Gemarkungen, eine WFS-Anfrage pro Schlüssel.
|
||||
|
||||
@@ -152,6 +173,7 @@ def flurstueck_alkis(
|
||||
vorgabe_namen: Optional[List[str]] = None,
|
||||
bbox=None,
|
||||
ohne_dialog: bool = False,
|
||||
ohne_schluessel_abfrage: bool = False,
|
||||
) -> Tuple[Optional[QgsVectorLayer], LoadStatus]:
|
||||
"""Lädt Flurstücke aus dem ALKIS-WFS anhand von Gemarkungsschlüsseln.
|
||||
|
||||
@@ -165,17 +187,27 @@ def flurstueck_alkis(
|
||||
Eltern-Widget für Dialoge.
|
||||
vorgabe_schluessel : list of str, optional
|
||||
Vorausgefüllte Gemarkungsnummern (4-stellig). Im Dialog als Standard
|
||||
angezeigt; bei ``ohne_dialog=True`` direkt verwendet.
|
||||
angezeigt; bei ``ohne_dialog=True`` oder ``ohne_schluessel_abfrage=True``
|
||||
direkt verwendet.
|
||||
vorgabe_namen : list of str, optional
|
||||
Menschenlesbare Gemarkungsbezeichnungen für den Dialog-Hinweistext.
|
||||
bbox :
|
||||
Nicht mehr verwendet (WFS unterstützt keinen BBox-Filter). Bleibt
|
||||
für Rückwärtskompatibilität in der Signatur erhalten.
|
||||
ohne_dialog : bool, optional
|
||||
``True``: Überspringt die Gemarkungsschlüssel-Nutzerabfrage und verwendet
|
||||
``vorgabe_schluessel`` direkt. Vorhandene Layer werden ohne Rückfrage
|
||||
ersetzt. Gedacht für die automatische Pipeline
|
||||
(``verfahrensgebiet_alkis_komplett``). Standard: ``False``.
|
||||
``True``: Überspringt sowohl die Gemarkungsschlüssel-Nutzerabfrage (wie
|
||||
``ohne_schluessel_abfrage``) als auch die Rückfrage bei einem bereits
|
||||
vorhandenen Layer – dieser wird dann immer ersetzt. Gedacht für die
|
||||
automatischen Pipelines (``verfahrensgebiet_alkis_komplett``,
|
||||
``verfahrensgebiet_shape_komplett``). Standard: ``False``.
|
||||
ohne_schluessel_abfrage : bool, optional
|
||||
``True``: Überspringt nur die Gemarkungsschlüssel-Nutzerabfrage und
|
||||
verwendet ``vorgabe_schluessel`` direkt; die Rückfrage bei einem
|
||||
bereits vorhandenen Layer (Überschreiben/Anhängen/Abbrechen, inkl.
|
||||
Dedup beim Anhängen) bleibt aktiv. Für Aufrufer, die die Schlüssel
|
||||
bereits anderweitig ermittelt haben (z. B. aus einer Layer-Auswahl),
|
||||
aber trotzdem interaktiv mit einem bestehenden Layer umgehen wollen.
|
||||
Wirkungslos, wenn ``ohne_dialog=True`` gesetzt ist. Standard: ``False``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -201,8 +233,8 @@ def flurstueck_alkis(
|
||||
append_target = existing
|
||||
|
||||
# --- Schritt 2: Gemarkungsschlüssel bestimmen -------------------------
|
||||
if ohne_dialog:
|
||||
# Pipeline-Modus: vorgabe_schluessel direkt verwenden, kein Dialog
|
||||
if ohne_dialog or ohne_schluessel_abfrage:
|
||||
# vorgabe_schluessel direkt verwenden, kein ask_text-Dialog
|
||||
if not vorgabe_schluessel:
|
||||
return None, LoadStatus.NONE
|
||||
schluessel_liste = [s.strip() for s in vorgabe_schluessel if s.strip()]
|
||||
@@ -250,7 +282,22 @@ def flurstueck_alkis(
|
||||
|
||||
# --- Anhängen an vorhandenen Layer ------------------------------------
|
||||
if status == LoadStatus.APPEND and append_target is not None:
|
||||
append_target.dataProvider().addFeatures(list(layer.getFeatures()))
|
||||
alle_feats = list(layer.getFeatures())
|
||||
neue_feats = alle_feats
|
||||
if layer.fields().indexFromName(FELD_FLURSTUECKSKENNZEICHEN) >= 0:
|
||||
vorhandene = _vorhandene_kennzeichen(append_target)
|
||||
neue_feats = [
|
||||
f for f in alle_feats
|
||||
if str(f[FELD_FLURSTUECKSKENNZEICHEN] or "").strip() not in vorhandene
|
||||
]
|
||||
uebersprungen = len(alle_feats) - len(neue_feats)
|
||||
if uebersprungen:
|
||||
_info(
|
||||
"Flurstücke",
|
||||
f"{uebersprungen} bereits vorhandene Flurstücke übersprungen.",
|
||||
duration=3,
|
||||
)
|
||||
append_target.dataProvider().addFeatures(neue_feats)
|
||||
append_target.updateExtents()
|
||||
append_target.triggerRepaint()
|
||||
return append_target, LoadStatus.APPEND
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
"""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))
|
||||
@@ -34,6 +34,7 @@ from sn_basis.functions import QgsVectorLayer, QgsProject
|
||||
from sn_basis.functions import QMessageBox
|
||||
from sn_basis.functions.message_wrapper import info, warning
|
||||
from sn_basis.functions.dialog_wrapper import create_progress_dialog
|
||||
from sn_basis.functions.ly_style_wrapper import apply_style
|
||||
|
||||
ALKIS_SF_URL = "https://geodienste.sachsen.de/aaa/public_alkis/sf/wfs"
|
||||
PUNKTORT_TA_TYPENAME = "adv:AX_PunktortTA"
|
||||
@@ -1652,6 +1653,7 @@ def grenzpunkte_gemarkungsweise(
|
||||
return None
|
||||
|
||||
project.addMapLayer(joined_layer)
|
||||
apply_style(joined_layer, "alkis_grenzpunkte.qml")
|
||||
|
||||
prog.set_value(prog_gesamt)
|
||||
prog.close()
|
||||
|
||||
@@ -261,12 +261,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
|
||||
"""Auswahl + Bau für den Landkreis-Fallback.
|
||||
|
||||
Zeigt :func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge.ask_landkreis_objekte`
|
||||
(Mehrfachauswahl inkl. "Alle auswählen" und "Nur laden"-Checkbox). Die
|
||||
ausgewählten Kombinationen werden anhand der ersten 5 Zeichen von
|
||||
``bezeichnung`` (= die eigentliche VKZ) gruppiert; je Gruppe wird EIN
|
||||
Feature erzeugt (Geometrien der Gruppe verschmolzen). Damit entsteht bei
|
||||
Auswahl mehrerer/aller Objekte eines Landkreises ein Layer mit mehreren
|
||||
Features – einem je tatsächlichem Verfahren, nicht ein Gesamt-Blob.
|
||||
(Mehrfachauswahl inkl. "Alle auswählen"). Die ausgewählten Kombinationen
|
||||
werden anhand der ersten 5 Zeichen von ``bezeichnung`` (= die eigentliche
|
||||
VKZ) gruppiert; je Gruppe wird EIN Feature erzeugt (Geometrien der
|
||||
Gruppe verschmolzen). Damit entsteht bei Auswahl mehrerer/aller Objekte
|
||||
eines Landkreises ein Layer mit mehreren Features – einem je
|
||||
tatsächlichem Verfahren, nicht ein Gesamt-Blob.
|
||||
|
||||
Args:
|
||||
tab_widget: Parent-Widget für Dialoge und Meldungen
|
||||
@@ -275,13 +275,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
|
||||
status: Lade-Status aus Phase 1 (FIRST/RELOAD), wird durchgereicht
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[QgsVectorLayer], LoadStatus, bool]:
|
||||
``(layer, status, nur_laden)``.
|
||||
Tuple[Optional[QgsVectorLayer], LoadStatus]: ``(layer, status)``.
|
||||
"""
|
||||
key_list = sorted(kombinationen.keys())
|
||||
selected_keys, nur_laden = ask_landkreis_objekte(tab_widget, key_list)
|
||||
selected_keys = ask_landkreis_objekte(tab_widget, key_list)
|
||||
if not selected_keys:
|
||||
return None, LoadStatus.NONE, False
|
||||
return None, LoadStatus.NONE
|
||||
|
||||
# Nach VKZ (erste 5 Zeichen der bezeichnung) gruppieren
|
||||
vkz_gruppen: dict[str, list] = {}
|
||||
@@ -322,12 +321,12 @@ def _baue_layer_aus_landkreis_auswahl(tab_widget, kombinationen: dict, crs: str,
|
||||
|
||||
if not neue_features:
|
||||
QMessageBox.critical(tab_widget, "Fehler", "Keine Geometrien zum Verschmelzen gefunden.")
|
||||
return None, LoadStatus.NONE, False
|
||||
return None, LoadStatus.NONE
|
||||
|
||||
pr.addFeatures(neue_features)
|
||||
memory_layer.updateExtents()
|
||||
|
||||
return memory_layer, status, nur_laden
|
||||
return memory_layer, status
|
||||
|
||||
|
||||
def verfahrensgebiet_alkis(tab_widget, skip_existing_check: bool = False):
|
||||
|
||||
@@ -51,6 +51,7 @@ from sn_basis.functions.dialog_wrapper import (
|
||||
create_progress_dialog,
|
||||
show_info_dialog,
|
||||
ask_detailpruefung_oder_vg_laden,
|
||||
ask_yes_no,
|
||||
)
|
||||
from sn_basis.functions.landkreise import LANDKREISE, ALLE_LANDKREISE
|
||||
from sn_basis.functions.ly_style_wrapper import apply_style
|
||||
@@ -273,9 +274,13 @@ def verfahrensgebiet_alkis_komplett(
|
||||
durchsuchen) und wird danach immer geschlossen – auch wenn anschließend
|
||||
der BROR-Auswahldialog erscheint (kein Fortschrittsdialog läuft
|
||||
währenddessen parallel zum Auswahldialog).
|
||||
- **STOP** (Zwischenabfrage "Weiter?"): erscheint nach dem Laden/Übernehmen
|
||||
des BROR-Objekts, bevor Dialog 2 startet. Bei "Nein" bricht der Ablauf
|
||||
ab, das BROR-Objekt bleibt aber im Projekt (ersetzt die frühere
|
||||
"Nur Objekte laden"-Option des Landkreis-Auswahldialogs).
|
||||
- **Dialog 2** (eigentlicher Workflow, 6 Schritte): startet erst, nachdem
|
||||
das Verfahrensgebiet feststeht (direkt nach Dialog 1 bei eindeutigem
|
||||
Treffer/KEEP, oder erst nach Bestätigung des Auswahldialogs mit OK).
|
||||
das Verfahrensgebiet feststeht und die Stop-Abfrage mit "Ja" bestätigt
|
||||
wurde.
|
||||
|
||||
Schrittfolge in Dialog 2:
|
||||
|
||||
@@ -351,8 +356,6 @@ def verfahrensgebiet_alkis_komplett(
|
||||
# Dialog 1 schließt hier immer – egal ob eine Auswahl folgt oder nicht.
|
||||
progress_suche.close()
|
||||
|
||||
nur_laden = False
|
||||
|
||||
try:
|
||||
if vorab_layer is not None or not kombinationen:
|
||||
# KEEP oder Fehler/Abbruch bereits in Phase 1 entschieden
|
||||
@@ -364,7 +367,7 @@ def verfahrensgebiet_alkis_komplett(
|
||||
# Objekt(e) des Landkreises auswählen – kein eigener Fortschritts-
|
||||
# dialog währenddessen, nur der Auswahldialog selbst.
|
||||
# ------------------------------------------------------------------
|
||||
alkis_vg_layer, vg_status, nur_laden = _baue_layer_aus_landkreis_auswahl(
|
||||
alkis_vg_layer, vg_status = _baue_layer_aus_landkreis_auswahl(
|
||||
tab_widget, kombinationen, crs, vg_status
|
||||
)
|
||||
if vg_status == LoadStatus.NONE or not alkis_vg_layer:
|
||||
@@ -387,12 +390,21 @@ def verfahrensgebiet_alkis_komplett(
|
||||
project.addMapLayer(alkis_vg_layer)
|
||||
bror_was_added_by_us = True
|
||||
# Style direkt nach dem Laden anwenden – unabhängig vom weiteren
|
||||
# Ablauf, damit auch "Nur Objekte laden" den Style erhält.
|
||||
# Ablauf, damit auch bei "Nein" auf die Stop-Abfrage der Style
|
||||
# gesetzt ist.
|
||||
apply_style(alkis_vg_layer, BROR_STYLE_QML)
|
||||
|
||||
if nur_laden:
|
||||
# Nutzer hat "Nur laden" angehakt – kein Dialog 2, kein weiterer
|
||||
# Workflow (Gemarkung/Flurstücke/Vergleich).
|
||||
# ------------------------------------------------------------------
|
||||
# STOP: Zwischenabfrage nach dem Laden des BROR-Objekts, bevor mit
|
||||
# Gemarkung/Flurstücke/Flächenvergleich fortgefahren wird (ersetzt
|
||||
# die frühere "Nur Objekte laden"-Option).
|
||||
# ------------------------------------------------------------------
|
||||
if not ask_yes_no(
|
||||
"Verfahrensgebiet laden",
|
||||
f"Layer '{alkis_vg_layer.name()}' wurde geladen. Weiter?",
|
||||
default=True,
|
||||
parent=tab_widget,
|
||||
):
|
||||
abgeschlossen = True
|
||||
return alkis_vg_layer, None, None, vg_status
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"""sn_verfahrensgebiet/functions/verfahrensgebiet_dialoge.py – Dialoge zum
|
||||
Laden eines Verfahrensgebiets aus ALKIS (Qt5/6/Mock-kompatibel).
|
||||
|
||||
Enthält außerdem die gemeinsame Auswahltabelle (:func:`baue_auswahltabelle`),
|
||||
die auch die Eigentümer-Trefferliste (#56) nutzt.
|
||||
"""
|
||||
from typing import Any, Optional
|
||||
from sn_basis.functions.qt_wrapper import (
|
||||
@@ -8,6 +11,7 @@ from sn_basis.functions.qt_wrapper import (
|
||||
QTableWidget, QTableWidgetItem, QTableWidgetSelectionRange,
|
||||
QIcon, QVBoxLayout, QHBoxLayout, QLabel, QCheckBox,
|
||||
DialogAccepted, ExtendedSelection, SelectRows, NoEditTriggers, AscendingOrder,
|
||||
UserRole,
|
||||
)
|
||||
from sn_basis.functions.landkreise import ALLE_LANDKREISE
|
||||
import os
|
||||
@@ -17,6 +21,98 @@ _PLUGINS_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
_WAPPEN_ICON = os.path.join(_PLUGINS_DIR, "sn_basis", "favicon.ico")
|
||||
|
||||
|
||||
def zeilen_sichtbar(zeilen_texte: list[list[str]], suchtext: str) -> list[bool]:
|
||||
"""Ob jede Tabellenzeile zum Suchtext passt: Teilstring in irgendeiner Spalte,
|
||||
ohne Groß-/Kleinschreibung. Ein leerer Suchtext zeigt alle Zeilen."""
|
||||
such = (suchtext or "").strip().lower()
|
||||
return [not such or any(such in (t or "").lower() for t in texte) for texte in zeilen_texte]
|
||||
|
||||
|
||||
def baue_auswahltabelle(
|
||||
layout: Any,
|
||||
spaltenkoepfe: list[str],
|
||||
zeilen: list[tuple[str, ...]],
|
||||
platzhalter: str = "Suche …",
|
||||
alle_auswaehlen: bool = True,
|
||||
) -> Any:
|
||||
"""Fügt Suchfeld, „Alle auswählen“, Auswahltabelle und Mehrfachauswahl-Hinweis
|
||||
in ``layout`` ein und gibt die Tabelle zurück.
|
||||
|
||||
Gemeinsamer Baustein von :func:`ask_landkreis_objekte` und der
|
||||
Eigentümer-Trefferliste (#56). Die Tabelle ist nach Spalte 0 aufsteigend
|
||||
sortiert, per Klick auf den Spaltenkopf umsortierbar und erlaubt die
|
||||
Mehrfachauswahl ganzer Zeilen. Spalte 0 trägt den Index der Zeile in
|
||||
``zeilen`` (``UserRole``), damit die Auswahl auch nach dem Umsortieren
|
||||
zugeordnet werden kann (:func:`ausgewaehlte_indizes`).
|
||||
"""
|
||||
suche_zeile = QHBoxLayout()
|
||||
suche_zeile.addWidget(QLabel("Suche:"))
|
||||
suche_feld = QLineEdit()
|
||||
suche_feld.setPlaceholderText(platzhalter)
|
||||
suche_zeile.addWidget(suche_feld)
|
||||
layout.addLayout(suche_zeile)
|
||||
|
||||
alle_checkbox = None
|
||||
if alle_auswaehlen:
|
||||
alle_checkbox = QCheckBox("Alle auswählen")
|
||||
layout.addWidget(alle_checkbox)
|
||||
|
||||
table = QTableWidget(len(zeilen), len(spaltenkoepfe))
|
||||
table.setHorizontalHeaderLabels(spaltenkoepfe)
|
||||
table.setSelectionMode(ExtendedSelection)
|
||||
table.setSelectionBehavior(SelectRows)
|
||||
table.setEditTriggers(NoEditTriggers)
|
||||
table.verticalHeader().setVisible(False)
|
||||
table.verticalHeader().setDefaultSectionSize(22) # flachere Zeilen
|
||||
table.horizontalHeader().setStretchLastSection(True)
|
||||
# Sortierung erst nach dem Befüllen einschalten, sonst wandern Zeilen beim setItem
|
||||
table.setSortingEnabled(False)
|
||||
for row, texte in enumerate(zeilen):
|
||||
for col, text in enumerate(texte):
|
||||
item = QTableWidgetItem(text)
|
||||
if col == 0:
|
||||
item.setData(UserRole, row)
|
||||
table.setItem(row, col, item)
|
||||
table.setSortingEnabled(True)
|
||||
table.sortByColumn(0, AscendingOrder)
|
||||
layout.addWidget(table, 1) # Stretch-Faktor: wächst mit Fenstergröße
|
||||
|
||||
def _suche_geaendert(text):
|
||||
texte = [
|
||||
[table.item(row, col).text() for col in range(table.columnCount())]
|
||||
for row in range(table.rowCount())
|
||||
]
|
||||
for row, sichtbar in enumerate(zeilen_sichtbar(texte, text)):
|
||||
table.setRowHidden(row, not sichtbar)
|
||||
|
||||
suche_feld.textChanged.connect(_suche_geaendert)
|
||||
|
||||
def _alle_auswaehlen_umschalten(checked):
|
||||
table.clearSelection()
|
||||
if checked:
|
||||
letzte_spalte = table.columnCount() - 1
|
||||
for row in range(table.rowCount()):
|
||||
if not table.isRowHidden(row):
|
||||
# setRangeSelected (statt selectRow) fügt der Selektion
|
||||
# hinzu, ohne vorherige Zeilen wieder abzuwählen.
|
||||
table.setRangeSelected(
|
||||
QTableWidgetSelectionRange(row, 0, row, letzte_spalte), True
|
||||
)
|
||||
|
||||
if alle_checkbox is not None:
|
||||
alle_checkbox.stateChanged.connect(_alle_auswaehlen_umschalten)
|
||||
|
||||
layout.addWidget(QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>"))
|
||||
return table
|
||||
|
||||
|
||||
def ausgewaehlte_indizes(table: Any) -> list[int]:
|
||||
"""Indizes der ausgewählten Zeilen, bezogen auf ``zeilen`` aus
|
||||
:func:`baue_auswahltabelle`, aufsteigend."""
|
||||
zeilen = {item.row() for item in table.selectedItems()}
|
||||
return sorted(int(table.item(row, 0).data(UserRole)) for row in zeilen)
|
||||
|
||||
|
||||
def ask_bror_kombinationen(
|
||||
parent: Any,
|
||||
kombinationen: list[tuple[str, str]],
|
||||
@@ -101,15 +197,15 @@ def ask_bror_kombinationen(
|
||||
def ask_landkreis_objekte(
|
||||
parent: Any,
|
||||
kombinationen: list[tuple[str, str]],
|
||||
) -> tuple[list[tuple[str, str]], bool]:
|
||||
) -> list[tuple[str, str]]:
|
||||
"""Zeigt Dialog zur Auswahl eines, mehrerer oder aller BROR-Objekte eines
|
||||
Landkreises (Fallback, wenn keine Verfahrensnummer gesetzt ist).
|
||||
|
||||
Wie :func:`ask_bror_kombinationen`, zusätzlich mit einer "Alle
|
||||
auswählen"-Checkbox (da hier potenziell deutlich mehr Treffer als bei der
|
||||
VKZ-Suche auftreten) und einer "Nur laden"-Checkbox, mit der gesteuert
|
||||
wird, ob nach dem Laden der weitere Workflow (Gemarkung/Flurstücke/
|
||||
Flächenvergleich) fortgesetzt werden soll.
|
||||
VKZ-Suche auftreten). Ob nach dem Laden der weitere Workflow
|
||||
(Gemarkung/Flurstücke/Flächenvergleich) fortgesetzt wird, klärt eine
|
||||
separate Stop-Abfrage nach dem Laden – nicht mehr dieser Dialog.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -120,16 +216,16 @@ def ask_landkreis_objekte(
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[list[tuple[str, str]], bool]
|
||||
``(ausgewaehlte_kombinationen, nur_laden)``. ``([], False)`` bei
|
||||
list[tuple[str, str]]
|
||||
Die gewählten ``(adv_name, bezeichnung)``-Tupel. Leere Liste bei
|
||||
Abbruch oder wenn nichts ausgewählt wurde.
|
||||
"""
|
||||
if not kombinationen:
|
||||
return [], False
|
||||
return []
|
||||
|
||||
if QT_VERSION == 0: # Mock-Modus
|
||||
print(f"Mock-Modus: ask_landkreis_objekte → {kombinationen[:1]}")
|
||||
return kombinationen[:1], False
|
||||
return kombinationen[:1]
|
||||
|
||||
dialog = QDialog(parent)
|
||||
dialog.setWindowTitle("Verfahrensgebiete aus Landkreis laden")
|
||||
@@ -144,68 +240,17 @@ def ask_landkreis_objekte(
|
||||
hinweis.setWordWrap(True)
|
||||
layout.addWidget(hinweis)
|
||||
|
||||
suche_zeile = QHBoxLayout()
|
||||
suche_zeile.addWidget(QLabel("Suche:"))
|
||||
suche_feld = QLineEdit()
|
||||
suche_feld.setPlaceholderText("Nummer oder Name …")
|
||||
suche_zeile.addWidget(suche_feld)
|
||||
layout.addLayout(suche_zeile)
|
||||
|
||||
alle_auswaehlen = QCheckBox("Alle auswählen")
|
||||
layout.addWidget(alle_auswaehlen)
|
||||
|
||||
# Standardsortierung nach Bezeichnung; per Klick auf Spaltenkopf änderbar
|
||||
# (setSortingEnabled aktiviert die eingebaute Tabellensortierung).
|
||||
# Standardsortierung nach Bezeichnung; per Klick auf Spaltenkopf änderbar.
|
||||
sortierte_kombinationen = sorted(kombinationen, key=lambda kb: kb[1])
|
||||
|
||||
table = QTableWidget(len(sortierte_kombinationen), 2)
|
||||
table.setHorizontalHeaderLabels(["Nummer", "Name"])
|
||||
table.setSelectionMode(ExtendedSelection)
|
||||
table.setSelectionBehavior(SelectRows)
|
||||
table.setEditTriggers(NoEditTriggers)
|
||||
table.verticalHeader().setVisible(False)
|
||||
table.verticalHeader().setDefaultSectionSize(22) # flachere Zeilen
|
||||
table.horizontalHeader().setStretchLastSection(True)
|
||||
table.setSortingEnabled(False)
|
||||
for row, (adv, bez) in enumerate(sortierte_kombinationen):
|
||||
table.setItem(row, 0, QTableWidgetItem(bez))
|
||||
table.setItem(row, 1, QTableWidgetItem(adv))
|
||||
table.setSortingEnabled(True)
|
||||
table.sortByColumn(0, AscendingOrder) # Standard: aufsteigend nach Nummer
|
||||
table = baue_auswahltabelle(
|
||||
layout,
|
||||
["Nummer", "Name"],
|
||||
[(bez, adv) for adv, bez in sortierte_kombinationen],
|
||||
platzhalter="Nummer oder Name …",
|
||||
)
|
||||
table.resizeColumnToContents(0)
|
||||
table.setColumnWidth(0, max(110, table.columnWidth(0)))
|
||||
table.selectRow(0)
|
||||
layout.addWidget(table, 1) # Stretch-Faktor: wächst mit Fenstergröße
|
||||
|
||||
def _suche_geaendert(text):
|
||||
text_lower = text.strip().lower()
|
||||
for row in range(table.rowCount()):
|
||||
bez_text = table.item(row, 0).text().lower()
|
||||
adv_text = table.item(row, 1).text().lower()
|
||||
treffer = not text_lower or text_lower in bez_text or text_lower in adv_text
|
||||
table.setRowHidden(row, not treffer)
|
||||
|
||||
suche_feld.textChanged.connect(_suche_geaendert)
|
||||
|
||||
def _alle_auswaehlen_umschalten(checked):
|
||||
table.clearSelection()
|
||||
if checked:
|
||||
letzte_spalte = table.columnCount() - 1
|
||||
for row in range(table.rowCount()):
|
||||
if not table.isRowHidden(row):
|
||||
# setRangeSelected (statt selectRow) fügt der Selektion
|
||||
# hinzu, ohne vorherige Zeilen wieder abzuwählen.
|
||||
table.setRangeSelected(
|
||||
QTableWidgetSelectionRange(row, 0, row, letzte_spalte), True
|
||||
)
|
||||
|
||||
alle_auswaehlen.stateChanged.connect(_alle_auswaehlen_umschalten)
|
||||
|
||||
mehrfachauswahl_hinweis = QLabel("<small>Mehrfachauswahl mit Strg + Umschalt</small>")
|
||||
layout.addWidget(mehrfachauswahl_hinweis)
|
||||
|
||||
nur_laden_checkbox = QCheckBox("Nur Objekte laden (ohne weitere Prüfungen)")
|
||||
layout.addWidget(nur_laden_checkbox)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
buttons.accepted.connect(dialog.accept)
|
||||
@@ -235,11 +280,11 @@ def ask_landkreis_objekte(
|
||||
dialog.resize(breite, dialog.sizeHint().height())
|
||||
|
||||
if exec_dialog(dialog) != DialogAccepted:
|
||||
return [], False
|
||||
return []
|
||||
|
||||
ausgewaehlte_zeilen = sorted(set(item.row() for item in table.selectedItems()))
|
||||
ausgewaehlt = [(table.item(row, 1).text(), table.item(row, 0).text()) for row in ausgewaehlte_zeilen]
|
||||
return ausgewaehlt, nur_laden_checkbox.isChecked()
|
||||
return ausgewaehlt
|
||||
|
||||
|
||||
def ask_landkreis_auswahl(
|
||||
|
||||
@@ -42,6 +42,7 @@ oder kein Ergebnis liefert, bricht den Workflow ab.
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
import zipfile
|
||||
@@ -106,6 +107,57 @@ _GEBIETSTYP_LAYER_NAMEN = {
|
||||
# Interne Hilfsfunktionen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _normalisiere_feldname(name: str) -> str:
|
||||
"""Normalisiert einen Feldnamen für den toleranten Vergleich.
|
||||
|
||||
Entfernt alle Zeichen außer Buchstaben/Ziffern (z.B. Punkte, Leerzeichen,
|
||||
Unterstriche) und wandelt in Kleinbuchstaben um. Hintergrund: manche
|
||||
Shape-Exporte liefern den Feldnamen ``FBSchl`` mit einem angehängten
|
||||
Punkt (``FBSchl.``) oder abweichender Groß-/Kleinschreibung, wodurch ein
|
||||
exakter Namensvergleich das Feld fälschlich als fehlend meldet (#44).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name :
|
||||
Roher Feldname.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Normalisierter Feldname (nur Kleinbuchstaben/Ziffern).
|
||||
"""
|
||||
return re.sub(r"[^a-zA-Z0-9]", "", name).lower()
|
||||
|
||||
|
||||
def _finde_fbschl_index(raw_layer) -> int:
|
||||
"""Sucht den Feldindex von ``FBSchl`` tolerant gegenüber Abweichungen im
|
||||
Namen (Groß-/Kleinschreibung, angehängte Sonderzeichen wie ein Punkt).
|
||||
|
||||
Versucht zuerst den exakten Namen; schlägt das fehl, wird über alle
|
||||
Felder mit normalisiertem Namensvergleich gesucht (siehe
|
||||
:func:`_normalisiere_feldname`).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_layer :
|
||||
Dateibasierter Shapefile-Layer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
Feldindex, oder ``-1`` wenn kein passendes Feld gefunden wurde.
|
||||
"""
|
||||
idx = raw_layer.fields().indexFromName(FBSCHL_FELD)
|
||||
if idx >= 0:
|
||||
return idx
|
||||
|
||||
ziel = _normalisiere_feldname(FBSCHL_FELD)
|
||||
for feld_idx, feld in enumerate(raw_layer.fields()):
|
||||
if _normalisiere_feldname(feld.name()) == ziel:
|
||||
return feld_idx
|
||||
return -1
|
||||
|
||||
|
||||
def _ermittle_gebietstyp_aus_fbschl(raw_layer) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""Prüft das Feld ``FBSchl`` und ermittelt den Gebietstyp.
|
||||
|
||||
@@ -113,6 +165,10 @@ def _ermittle_gebietstyp_aus_fbschl(raw_layer) -> Tuple[Optional[str], Optional[
|
||||
zurück, damit der Aufrufer Fehler mehrerer Dateien sammeln und am Ende
|
||||
gemeinsam anzeigen kann.
|
||||
|
||||
Die Feldsuche ist tolerant gegenüber Abweichungen im Namen (z.B. ein
|
||||
angehängter Punkt ``FBSchl.`` in manchen Shape-Exporten, siehe #44) –
|
||||
siehe :func:`_finde_fbschl_index`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_layer :
|
||||
@@ -127,7 +183,7 @@ def _ermittle_gebietstyp_aus_fbschl(raw_layer) -> Tuple[Optional[str], Optional[
|
||||
``fehlertext`` beschreibt die Ursache (fehlendes Feld oder
|
||||
unbekannter Wert).
|
||||
"""
|
||||
idx = raw_layer.fields().indexFromName(FBSCHL_FELD)
|
||||
idx = _finde_fbschl_index(raw_layer)
|
||||
if idx < 0:
|
||||
return None, f"Feld '{FBSCHL_FELD}' fehlt (Shape entspricht nicht dem erwarteten Inhalt)."
|
||||
|
||||
|
||||
@@ -0,0 +1,913 @@
|
||||
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
|
||||
<qgis autoRefreshMode="Disabled" autoRefreshTime="0" hasScaleBasedVisibilityFlag="0" labelsEnabled="0" layerType="Vector" maxScale="0" minScale="100000000" readOnly="0" simplifyAlgorithm="0" simplifyDrawingHints="0" simplifyDrawingTol="1" simplifyLocal="1" simplifyMaxScale="1" styleCategories="AllStyleCategories" symbologyReferenceScale="-1" version="4.2.0-Belém do Pará">
|
||||
<flags>
|
||||
<Identifiable>1</Identifiable>
|
||||
<Removable>1</Removable>
|
||||
<Searchable>1</Searchable>
|
||||
<Private>0</Private>
|
||||
</flags>
|
||||
<temporal accumulate="0" durationField="" durationUnit="min" enabled="0" endExpression="" endField="" fixedDuration="0" limitMode="0" mode="0" startExpression="" startField="">
|
||||
<fixedRange>
|
||||
<start></start>
|
||||
<end></end>
|
||||
</fixedRange>
|
||||
</temporal>
|
||||
<elevation binding="Centroid" clamping="Terrain" customToleranceEnabled="0" extrusion="0" extrusionEnabled="0" respectLayerSymbol="1" showMarkerSymbolInSurfacePlots="0" symbology="Line" type="IndividualFeatures" zoffset="0" zscale="1">
|
||||
<data-defined-properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data-defined-properties>
|
||||
<profileLineSymbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="line">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleLine" enabled="1" id="{524b77c5-d547-434b-918a-eaa8d67032a3}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="align_dash_pattern" type="QString" value="0"/>
|
||||
<Option name="capstyle" type="QString" value="square"/>
|
||||
<Option name="customdash" type="QString" value="5;2"/>
|
||||
<Option name="customdash_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="customdash_unit" type="QString" value="MM"/>
|
||||
<Option name="dash_pattern_offset" type="QString" value="0"/>
|
||||
<Option name="dash_pattern_offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="dash_pattern_offset_unit" type="QString" value="MM"/>
|
||||
<Option name="draw_inside_polygon" type="QString" value="0"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="line_color" type="QString" value="152,125,183,255,rgb:0.5960785,0.4901961,0.7176471,1"/>
|
||||
<Option name="line_style" type="QString" value="solid"/>
|
||||
<Option name="line_width" type="QString" value="0.6"/>
|
||||
<Option name="line_width_unit" type="QString" value="MM"/>
|
||||
<Option name="offset" type="QString" value="0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="ring_filter" type="QString" value="0"/>
|
||||
<Option name="trim_distance_end" type="QString" value="0"/>
|
||||
<Option name="trim_distance_end_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="trim_distance_end_unit" type="QString" value="MM"/>
|
||||
<Option name="trim_distance_start" type="QString" value="0"/>
|
||||
<Option name="trim_distance_start_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="trim_distance_start_unit" type="QString" value="MM"/>
|
||||
<Option name="tweak_dash_pattern_on_corners" type="QString" value="0"/>
|
||||
<Option name="use_custom_dash" type="QString" value="0"/>
|
||||
<Option name="width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</profileLineSymbol>
|
||||
<profileFillSymbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="fill">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleFill" enabled="1" id="{a950bd48-1020-4bd8-ba91-53a188c9d8c0}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="border_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="color" type="QString" value="152,125,183,255,rgb:0.5960785,0.4901961,0.7176471,1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="109,89,131,255,rgb:0.4257572,0.3501335,0.5125963,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.2"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="style" type="QString" value="solid"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</profileFillSymbol>
|
||||
<profileMarkerSymbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{1432e3c3-aaae-4f1c-8df7-441e77bdd112}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="152,125,183,255,rgb:0.5960785,0.4901961,0.7176471,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="diamond"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="109,89,131,255,rgb:0.4257572,0.3501335,0.5125963,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0.2"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="3"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="MM"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</profileMarkerSymbol>
|
||||
</elevation>
|
||||
<renderer-v2 enableorderby="0" forceraster="0" referencescale="-1" symbollevels="0" type="RuleRenderer">
|
||||
<rules key="{b5ed05de-e0db-4029-a532-cc0c11943124}">
|
||||
<rule filter=" "gp_abmarkung_Marke" = '1000' " key="{ba76030e-eb1e-4a5a-b10c-b8614af46b1a}" label="Abgemarkt" symbol="0">
|
||||
<rule filter=""gp_festgestellterGrenzpunkt" = 'true'" key="{0f910cf3-1e11-45f4-b4a5-5b8b11f4e2f3}" label="FGP" symbol="1"/>
|
||||
</rule>
|
||||
<rule filter=" "gp_abmarkung_Marke" IN ('9500')" key="{f530f073-c1a2-426e-9394-eb137d98b1fd}" label="Nicht abgemarkt" symbol="2">
|
||||
<rule filter=""gp_festgestellterGrenzpunkt" = 'true'" key="{c1a0a382-cbf0-4981-b461-33c42c64701e}" label="FGP" symbol="3"/>
|
||||
</rule>
|
||||
<rule filter=" "gp_abmarkung_Marke" IN ('9600')" key="{f9d78633-b6a7-40df-88b4-d2d5bd51d1ab}" label="zeitweilig ausgesetzt" symbol="4">
|
||||
<rule filter=""gp_festgestellterGrenzpunkt" = 'true'" key="{66a1f910-5127-4e2c-8c55-bb48f290be47}" label="FGP" symbol="5"/>
|
||||
</rule>
|
||||
</rules>
|
||||
<symbols>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="0" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{4b2b5770-c712-4a17-a7e7-e8d833a5ef55}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,255,255,255,hsv:0,0,1,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="Point"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="1"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="Point"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="6"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="1" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{e3e8ca93-babc-4c67-803f-73b091b1dcbf}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,255,255,0,hsv:0,0,1,0"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="Point"/>
|
||||
<Option name="outline_color" type="QString" value="227,26,28,255,rgb:0.8901961,0.1019608,0.1098039,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="1"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="Point"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="8"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="2" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{3b684a01-3cd1-4ec2-a3b3-5dd097cac078}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,255,255,255,hsv:0,0,1,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||
<Option name="outline_style" type="QString" value="no"/>
|
||||
<Option name="outline_width" type="QString" value="0"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="6"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
<layer class="SimpleMarker" enabled="1" id="{7cf47a18-2311-467c-835d-8a5ba1dcbbf9}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="0,0,0,255,hsv:0.99550002813339233,0.60656136274337769,0,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="2"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="3" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{e3e8ca93-babc-4c67-803f-73b091b1dcbf}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,255,255,0,hsv:0,0,1,0"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="Point"/>
|
||||
<Option name="outline_color" type="QString" value="227,26,28,255,rgb:0.8901961,0.1019608,0.1098039,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="1"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="Point"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="8"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="4" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{3b684a01-3cd1-4ec2-a3b3-5dd097cac078}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,255,255,255,hsv:0,0,1,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="1"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="6"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
<layer class="SimpleMarker" enabled="1" id="{7cf47a18-2311-467c-835d-8a5ba1dcbbf9}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="0,0,0,255,hsv:0.99550002813339233,0.60656136274337769,0,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="2"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="5" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{e3e8ca93-babc-4c67-803f-73b091b1dcbf}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,255,255,0,hsv:0,0,1,0"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="Point"/>
|
||||
<Option name="outline_color" type="QString" value="227,26,28,255,rgb:0.8901961,0.1019608,0.1098039,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="1"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="Point"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="8"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="Point"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</symbols>
|
||||
<data-defined-properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data-defined-properties>
|
||||
</renderer-v2>
|
||||
<selection mode="Default">
|
||||
<selectionColor invalid="1"/>
|
||||
<selectionSymbol>
|
||||
<symbol alpha="1" clip_to_extent="1" force_rhr="0" frame_rate="10" is_animated="0" name="" type="marker">
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
<layer class="SimpleMarker" enabled="1" id="{4e7caead-e027-4f3e-a2ff-5a600d1c387c}" locked="0" pass="0">
|
||||
<Option type="Map">
|
||||
<Option name="angle" type="QString" value="0"/>
|
||||
<Option name="cap_style" type="QString" value="square"/>
|
||||
<Option name="color" type="QString" value="255,0,0,255,rgb:1,0,0,1"/>
|
||||
<Option name="horizontal_anchor_point" type="QString" value="1"/>
|
||||
<Option name="joinstyle" type="QString" value="bevel"/>
|
||||
<Option name="name" type="QString" value="circle"/>
|
||||
<Option name="offset" type="QString" value="0,0"/>
|
||||
<Option name="offset_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="offset_unit" type="QString" value="MM"/>
|
||||
<Option name="outline_color" type="QString" value="35,35,35,255,rgb:0.1372549,0.1372549,0.1372549,1"/>
|
||||
<Option name="outline_style" type="QString" value="solid"/>
|
||||
<Option name="outline_width" type="QString" value="0"/>
|
||||
<Option name="outline_width_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="outline_width_unit" type="QString" value="MM"/>
|
||||
<Option name="scale_method" type="QString" value="diameter"/>
|
||||
<Option name="size" type="QString" value="2"/>
|
||||
<Option name="size_map_unit_scale" type="QString" value="3x:0,0,0,0,0,0"/>
|
||||
<Option name="size_unit" type="QString" value="MM"/>
|
||||
<Option name="vertical_anchor_point" type="QString" value="1"/>
|
||||
</Option>
|
||||
<data_defined_properties>
|
||||
<Option type="Map">
|
||||
<Option name="name" type="QString" value=""/>
|
||||
<Option name="properties"/>
|
||||
<Option name="type" type="QString" value="collection"/>
|
||||
</Option>
|
||||
</data_defined_properties>
|
||||
</layer>
|
||||
</symbol>
|
||||
</selectionSymbol>
|
||||
</selection>
|
||||
<customproperties>
|
||||
<Option type="Map">
|
||||
<Option name="dualview/previewExpressions" type="List">
|
||||
<Option type="QString" value="COALESCE( "gml_id", '<NULL>' )"/>
|
||||
</Option>
|
||||
<Option name="embeddedWidgets/count" type="int" value="0"/>
|
||||
<Option name="variableNames"/>
|
||||
<Option name="variableValues"/>
|
||||
</Option>
|
||||
</customproperties>
|
||||
<blendMode>0</blendMode>
|
||||
<featureBlendMode>0</featureBlendMode>
|
||||
<layerOpacity>1</layerOpacity>
|
||||
<geometryOptions geometryPrecision="0" removeDuplicateNodes="0">
|
||||
<activeChecks/>
|
||||
<checkConfiguration/>
|
||||
</geometryOptions>
|
||||
<legend showLabelLegend="0" type="default-vector"/>
|
||||
<referencedLayers/>
|
||||
<referencingLayers/>
|
||||
<fieldConfiguration>
|
||||
<field configurationFlags="0" name="gml_id">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="identifier">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="lebenszeitintervall">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="modellart">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="anlass">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="zeigtAufExternes">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="istTeilVon">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="kartendarstellung">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="koordinatenstatus">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_gml_id">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_identifier">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_lebenszeitintervall">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_modellart">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_anlass">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_zeigtAufExternes">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_bestehtAus">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_punktkennung">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_abmarkung_Marke">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_festgestellterGrenzpunkt">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_inversZu_zeigtAuf">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
<field configurationFlags="0" name="gp_sonstigeEigenschaft">
|
||||
<editWidget type="TextEdit">
|
||||
<config>
|
||||
<Option/>
|
||||
</config>
|
||||
</editWidget>
|
||||
</field>
|
||||
</fieldConfiguration>
|
||||
<aliases>
|
||||
<alias field="gml_id" index="0" name=""/>
|
||||
<alias field="identifier" index="1" name=""/>
|
||||
<alias field="lebenszeitintervall" index="2" name=""/>
|
||||
<alias field="modellart" index="3" name=""/>
|
||||
<alias field="anlass" index="4" name=""/>
|
||||
<alias field="zeigtAufExternes" index="5" name=""/>
|
||||
<alias field="istTeilVon" index="6" name=""/>
|
||||
<alias field="kartendarstellung" index="7" name=""/>
|
||||
<alias field="koordinatenstatus" index="8" name=""/>
|
||||
<alias field="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe" index="9" name=""/>
|
||||
<alias field="gp_gml_id" index="10" name=""/>
|
||||
<alias field="gp_identifier" index="11" name=""/>
|
||||
<alias field="gp_lebenszeitintervall" index="12" name=""/>
|
||||
<alias field="gp_modellart" index="13" name=""/>
|
||||
<alias field="gp_anlass" index="14" name=""/>
|
||||
<alias field="gp_zeigtAufExternes" index="15" name=""/>
|
||||
<alias field="gp_bestehtAus" index="16" name=""/>
|
||||
<alias field="gp_punktkennung" index="17" name=""/>
|
||||
<alias field="gp_abmarkung_Marke" index="18" name=""/>
|
||||
<alias field="gp_festgestellterGrenzpunkt" index="19" name=""/>
|
||||
<alias field="gp_inversZu_zeigtAuf" index="20" name=""/>
|
||||
<alias field="gp_sonstigeEigenschaft" index="21" name=""/>
|
||||
</aliases>
|
||||
<defaults>
|
||||
<default applyOnUpdate="0" expression="" field="gml_id"/>
|
||||
<default applyOnUpdate="0" expression="" field="identifier"/>
|
||||
<default applyOnUpdate="0" expression="" field="lebenszeitintervall"/>
|
||||
<default applyOnUpdate="0" expression="" field="modellart"/>
|
||||
<default applyOnUpdate="0" expression="" field="anlass"/>
|
||||
<default applyOnUpdate="0" expression="" field="zeigtAufExternes"/>
|
||||
<default applyOnUpdate="0" expression="" field="istTeilVon"/>
|
||||
<default applyOnUpdate="0" expression="" field="kartendarstellung"/>
|
||||
<default applyOnUpdate="0" expression="" field="koordinatenstatus"/>
|
||||
<default applyOnUpdate="0" expression="" field="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_gml_id"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_identifier"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_lebenszeitintervall"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_modellart"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_anlass"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_zeigtAufExternes"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_bestehtAus"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_punktkennung"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_abmarkung_Marke"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_festgestellterGrenzpunkt"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_inversZu_zeigtAuf"/>
|
||||
<default applyOnUpdate="0" expression="" field="gp_sonstigeEigenschaft"/>
|
||||
</defaults>
|
||||
<constraints>
|
||||
<constraint constraints="0" exp_strength="0" field="gml_id" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="identifier" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="lebenszeitintervall" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="modellart" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="anlass" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="zeigtAufExternes" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="istTeilVon" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="kartendarstellung" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="koordinatenstatus" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_gml_id" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_identifier" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_lebenszeitintervall" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_modellart" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_anlass" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_zeigtAufExternes" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_bestehtAus" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_punktkennung" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_abmarkung_Marke" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_festgestellterGrenzpunkt" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_inversZu_zeigtAuf" notnull_strength="0" unique_strength="0"/>
|
||||
<constraint constraints="0" exp_strength="0" field="gp_sonstigeEigenschaft" notnull_strength="0" unique_strength="0"/>
|
||||
</constraints>
|
||||
<constraintExpressions>
|
||||
<constraint desc="" exp="" field="gml_id"/>
|
||||
<constraint desc="" exp="" field="identifier"/>
|
||||
<constraint desc="" exp="" field="lebenszeitintervall"/>
|
||||
<constraint desc="" exp="" field="modellart"/>
|
||||
<constraint desc="" exp="" field="anlass"/>
|
||||
<constraint desc="" exp="" field="zeigtAufExternes"/>
|
||||
<constraint desc="" exp="" field="istTeilVon"/>
|
||||
<constraint desc="" exp="" field="kartendarstellung"/>
|
||||
<constraint desc="" exp="" field="koordinatenstatus"/>
|
||||
<constraint desc="" exp="" field="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"/>
|
||||
<constraint desc="" exp="" field="gp_gml_id"/>
|
||||
<constraint desc="" exp="" field="gp_identifier"/>
|
||||
<constraint desc="" exp="" field="gp_lebenszeitintervall"/>
|
||||
<constraint desc="" exp="" field="gp_modellart"/>
|
||||
<constraint desc="" exp="" field="gp_anlass"/>
|
||||
<constraint desc="" exp="" field="gp_zeigtAufExternes"/>
|
||||
<constraint desc="" exp="" field="gp_bestehtAus"/>
|
||||
<constraint desc="" exp="" field="gp_punktkennung"/>
|
||||
<constraint desc="" exp="" field="gp_abmarkung_Marke"/>
|
||||
<constraint desc="" exp="" field="gp_festgestellterGrenzpunkt"/>
|
||||
<constraint desc="" exp="" field="gp_inversZu_zeigtAuf"/>
|
||||
<constraint desc="" exp="" field="gp_sonstigeEigenschaft"/>
|
||||
</constraintExpressions>
|
||||
<expressionfields/>
|
||||
<attributeactions>
|
||||
<defaultAction key="Canvas" value="{00000000-0000-0000-0000-000000000000}"/>
|
||||
</attributeactions>
|
||||
<attributetableconfig actionWidgetStyle="dropDown" sortExpression="" sortOrder="0">
|
||||
<columns>
|
||||
<column hidden="0" name="gml_id" type="field" width="-1"/>
|
||||
<column hidden="0" name="identifier" type="field" width="-1"/>
|
||||
<column hidden="0" name="lebenszeitintervall" type="field" width="-1"/>
|
||||
<column hidden="0" name="modellart" type="field" width="-1"/>
|
||||
<column hidden="0" name="anlass" type="field" width="-1"/>
|
||||
<column hidden="0" name="zeigtAufExternes" type="field" width="-1"/>
|
||||
<column hidden="0" name="istTeilVon" type="field" width="-1"/>
|
||||
<column hidden="0" name="kartendarstellung" type="field" width="-1"/>
|
||||
<column hidden="0" name="koordinatenstatus" type="field" width="-1"/>
|
||||
<column hidden="0" name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_gml_id" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_identifier" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_lebenszeitintervall" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_modellart" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_anlass" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_zeigtAufExternes" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_bestehtAus" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_punktkennung" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_abmarkung_Marke" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_festgestellterGrenzpunkt" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_inversZu_zeigtAuf" type="field" width="-1"/>
|
||||
<column hidden="0" name="gp_sonstigeEigenschaft" type="field" width="-1"/>
|
||||
<column hidden="1" type="actions" width="-1"/>
|
||||
</columns>
|
||||
</attributetableconfig>
|
||||
<conditionalstyles>
|
||||
<rowstyles/>
|
||||
<fieldstyles/>
|
||||
</conditionalstyles>
|
||||
<storedexpressions/>
|
||||
<editform tolerant="1"></editform>
|
||||
<editforminit/>
|
||||
<editforminitcodesource>0</editforminitcodesource>
|
||||
<editforminitfilepath></editforminitfilepath>
|
||||
<editforminitcode><![CDATA[# -*- coding: utf-8 -*-
|
||||
"""
|
||||
QGIS-Formulare können eine Python-Funktion haben,, die aufgerufen wird, wenn sich das Formular öffnet
|
||||
|
||||
Diese Funktion kann verwendet werden um dem Formular Extralogik hinzuzufügen.
|
||||
|
||||
Der Name der Funktion wird im Feld "Python Init-Function" angegeben
|
||||
Ein Beispiel folgt:
|
||||
"""
|
||||
from qgis.PyQt.QtWidgets import QWidget
|
||||
|
||||
def my_form_open(dialog, layer, feature):
|
||||
geom = feature.geometry()
|
||||
control = dialog.findChild(QWidget, "MyLineEdit")
|
||||
]]></editforminitcode>
|
||||
<featformsuppress>0</featformsuppress>
|
||||
<editorlayout>generatedlayout</editorlayout>
|
||||
<editable>
|
||||
<field editable="1" name="anlass"/>
|
||||
<field editable="1" name="gml_id"/>
|
||||
<field editable="1" name="gp_abmarkung_Marke"/>
|
||||
<field editable="1" name="gp_anlass"/>
|
||||
<field editable="1" name="gp_bestehtAus"/>
|
||||
<field editable="1" name="gp_festgestellterGrenzpunkt"/>
|
||||
<field editable="1" name="gp_gml_id"/>
|
||||
<field editable="1" name="gp_identifier"/>
|
||||
<field editable="1" name="gp_inversZu_zeigtAuf"/>
|
||||
<field editable="1" name="gp_lebenszeitintervall"/>
|
||||
<field editable="1" name="gp_modellart"/>
|
||||
<field editable="1" name="gp_punktkennung"/>
|
||||
<field editable="1" name="gp_sonstigeEigenschaft"/>
|
||||
<field editable="1" name="gp_zeigtAufExternes"/>
|
||||
<field editable="1" name="identifier"/>
|
||||
<field editable="1" name="istTeilVon"/>
|
||||
<field editable="1" name="kartendarstellung"/>
|
||||
<field editable="1" name="koordinatenstatus"/>
|
||||
<field editable="1" name="lebenszeitintervall"/>
|
||||
<field editable="1" name="modellart"/>
|
||||
<field editable="1" name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"/>
|
||||
<field editable="1" name="zeigtAufExternes"/>
|
||||
</editable>
|
||||
<labelOnTop>
|
||||
<field labelOnTop="0" name="anlass"/>
|
||||
<field labelOnTop="0" name="gml_id"/>
|
||||
<field labelOnTop="0" name="gp_abmarkung_Marke"/>
|
||||
<field labelOnTop="0" name="gp_anlass"/>
|
||||
<field labelOnTop="0" name="gp_bestehtAus"/>
|
||||
<field labelOnTop="0" name="gp_festgestellterGrenzpunkt"/>
|
||||
<field labelOnTop="0" name="gp_gml_id"/>
|
||||
<field labelOnTop="0" name="gp_identifier"/>
|
||||
<field labelOnTop="0" name="gp_inversZu_zeigtAuf"/>
|
||||
<field labelOnTop="0" name="gp_lebenszeitintervall"/>
|
||||
<field labelOnTop="0" name="gp_modellart"/>
|
||||
<field labelOnTop="0" name="gp_punktkennung"/>
|
||||
<field labelOnTop="0" name="gp_sonstigeEigenschaft"/>
|
||||
<field labelOnTop="0" name="gp_zeigtAufExternes"/>
|
||||
<field labelOnTop="0" name="identifier"/>
|
||||
<field labelOnTop="0" name="istTeilVon"/>
|
||||
<field labelOnTop="0" name="kartendarstellung"/>
|
||||
<field labelOnTop="0" name="koordinatenstatus"/>
|
||||
<field labelOnTop="0" name="lebenszeitintervall"/>
|
||||
<field labelOnTop="0" name="modellart"/>
|
||||
<field labelOnTop="0" name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe"/>
|
||||
<field labelOnTop="0" name="zeigtAufExternes"/>
|
||||
</labelOnTop>
|
||||
<reuseLastValuePolicy>
|
||||
<field name="anlass" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gml_id" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_abmarkung_Marke" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_anlass" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_bestehtAus" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_festgestellterGrenzpunkt" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_gml_id" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_identifier" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_inversZu_zeigtAuf" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_lebenszeitintervall" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_modellart" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_punktkennung" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_sonstigeEigenschaft" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="gp_zeigtAufExternes" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="identifier" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="istTeilVon" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="kartendarstellung" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="koordinatenstatus" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="lebenszeitintervall" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="modellart" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="qualitaetsangaben_ax_dqpunktort_genauigkeitsstufe" reuseLastValuePolicy="NotAllowed"/>
|
||||
<field name="zeigtAufExternes" reuseLastValuePolicy="NotAllowed"/>
|
||||
</reuseLastValuePolicy>
|
||||
<dataDefinedFieldProperties/>
|
||||
<widgets/>
|
||||
<previewExpression>COALESCE( "gml_id", '<NULL>' )</previewExpression>
|
||||
<mapTip enabled="1"></mapTip>
|
||||
<layerGeometryType>0</layerGeometryType>
|
||||
</qgis>
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Tests für das Anzeigefenster des Eigentümer-Datenabgleichs (#56).
|
||||
|
||||
Alle Namen und Anschriften sind erfunden.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from sn_verfahrensgebiet.ui import eigentuemer_anzeige as ea
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer
|
||||
|
||||
ANNA = Eigentuemer(anrede="Frau", titel="Dr.", vorname="Anna", nachname="Beispiel", strasse="Hauptstraße 1",
|
||||
plz="01234", wohnort="Musterstadt", ortsteil="Unterdorf", emails=("anna@example.org",),
|
||||
verstorben=True, rechtsperson=("0",), per_ids=("4711",), konten=("98765",))
|
||||
|
||||
|
||||
class TestAnzeigetext(unittest.TestCase):
|
||||
|
||||
def test_name_anschrift_email(self):
|
||||
"""Prüft, dass die Anzeige Name mit Anrede und Titel, die mehrzeilige Anschrift mit Ortsteil und die E-Mail zeigt."""
|
||||
text = ea.anzeige_html([ANNA])
|
||||
self.assertIn("Frau Dr. Anna Beispiel", text)
|
||||
self.assertIn("Hauptstraße 1<br/>01234 Musterstadt<br/>OT Unterdorf", text)
|
||||
self.assertIn("anna@example.org", text)
|
||||
|
||||
def test_keine_internen_angaben(self):
|
||||
"""Prüft, dass der Eigentümer weder Konten, Personen-IDs, Rechtsperson-Wert noch das Kennzeichen „verstorben“ sieht."""
|
||||
text = ea.anzeige_html([ANNA])
|
||||
for intern in ("98765", "4711", "verstorben", "Rechtsperson"):
|
||||
self.assertNotIn(intern, text)
|
||||
|
||||
def test_fehlende_email_wird_genannt(self):
|
||||
"""Prüft, dass eine fehlende E-Mail ausdrücklich als „keine angegeben“ erscheint."""
|
||||
self.assertIn(ea.OHNE_EMAIL, ea.anzeige_html([Eigentuemer(nachname="Muster")]))
|
||||
|
||||
def test_werte_werden_maskiert(self):
|
||||
"""Prüft, dass Sonderzeichen aus den Daten maskiert werden und kein HTML einschleusen."""
|
||||
text = ea.anzeige_html([Eigentuemer(nachname="<b>Muster</b> & Co")])
|
||||
self.assertIn("<b>Muster</b> & Co", text)
|
||||
self.assertNotIn("<b>Muster", text)
|
||||
|
||||
def test_mehrere_eintraege_getrennt(self):
|
||||
"""Prüft, dass mehrere Einträge untereinander, durch eine Linie getrennt, erscheinen."""
|
||||
text = ea.anzeige_html([ANNA, Eigentuemer(vorname="Bernd", nachname="Muster")])
|
||||
self.assertEqual(text.count("<hr/>"), 1)
|
||||
self.assertIn("Bernd Muster", text)
|
||||
|
||||
def test_leer_nur_platzhalter(self):
|
||||
"""Prüft, dass die leere Anzeige nur den neutralen Platzhalter zeigt."""
|
||||
text = ea.anzeige_html([])
|
||||
self.assertIn(ea.PLATZHALTER, text)
|
||||
self.assertNotIn("Name", text)
|
||||
|
||||
|
||||
class TestFensterOeffnen(unittest.TestCase):
|
||||
|
||||
def test_normales_fenster_in_standardgroesse(self):
|
||||
"""Prüft, dass das Anzeigefenster ohne gemerkte Lage als normales Fenster mit Rahmen in Standardgröße öffnet, nie im Vollbild."""
|
||||
fenster = MagicMock()
|
||||
ea.zeige_anzeigefenster(fenster)
|
||||
fenster.resize.assert_called_once_with(*ea.STANDARDGROESSE)
|
||||
fenster.show.assert_called_once()
|
||||
fenster.showFullScreen.assert_not_called()
|
||||
fenster.setWindowFlags.assert_not_called()
|
||||
|
||||
def test_gemerkte_lage_wird_wiederhergestellt(self):
|
||||
"""Prüft, dass eine gemerkte Größe und Lage der letzten Sitzung wiederhergestellt wird."""
|
||||
fenster = MagicMock()
|
||||
fenster.restoreGeometry.return_value = True
|
||||
ea.zeige_anzeigefenster(fenster, b"geometrie")
|
||||
fenster.restoreGeometry.assert_called_once_with(b"geometrie")
|
||||
fenster.resize.assert_not_called()
|
||||
fenster.show.assert_called_once()
|
||||
fenster.showFullScreen.assert_not_called()
|
||||
|
||||
def test_unbrauchbare_lage_ergibt_standardgroesse(self):
|
||||
"""Prüft, dass eine nicht wiederherstellbare Lage auf die Standardgröße zurückfällt."""
|
||||
for ergebnis in (False, RuntimeError("kaputt")):
|
||||
with self.subTest(ergebnis=ergebnis):
|
||||
fenster = MagicMock()
|
||||
if isinstance(ergebnis, Exception):
|
||||
fenster.restoreGeometry.side_effect = ergebnis
|
||||
else:
|
||||
fenster.restoreGeometry.return_value = ergebnis
|
||||
ea.zeige_anzeigefenster(fenster, b"alt")
|
||||
fenster.resize.assert_called_once_with(*ea.STANDARDGROESSE)
|
||||
fenster.show.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Tests für die Korrekturen beim Eigentümer-Datenabgleich (#56).
|
||||
|
||||
Alle Namen und Anschriften sind erfunden.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
|
||||
from sn_verfahrensgebiet.functions import eigentuemer_korrektur as ek
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer
|
||||
|
||||
ANNA = Eigentuemer(anrede="Frau", vorname="Anna", nachname="Beispiel", strasse="Hauptstraße 1", plz="01234",
|
||||
wohnort="Musterstadt", emails=("anna@example.org",), per_ids=("7",), konten=("100", "200"))
|
||||
|
||||
|
||||
class TestAenderungen(unittest.TestCase):
|
||||
|
||||
def test_feldwerte(self):
|
||||
"""Prüft, dass alle korrigierbaren Felder vorbelegt werden, mehrere E-Mails durch Komma getrennt."""
|
||||
werte = ek.feldwerte(Eigentuemer(nachname="Beispiel", emails=("a@example.org", "b@example.org")))
|
||||
self.assertEqual(set(werte), {k for k, _ in ek.FELDER})
|
||||
self.assertEqual(werte["email"], "a@example.org, b@example.org")
|
||||
self.assertEqual(werte["nachname"], "Beispiel")
|
||||
|
||||
def test_nur_echte_aenderungen(self):
|
||||
"""Prüft, dass nur geänderte Felder zählen und Leerraum am Rand oder doppelte Leerzeichen keine Änderung sind."""
|
||||
werte = ek.feldwerte(ANNA)
|
||||
werte.update(strasse=" Hauptstraße 1 ", wohnort="Neustadt", email="anna.neu@example.org")
|
||||
self.assertEqual(ek.ermittle_aenderungen(ANNA, werte), [
|
||||
("wohnort", "Musterstadt", "Neustadt"),
|
||||
("email", "anna@example.org", "anna.neu@example.org"),
|
||||
])
|
||||
self.assertEqual(ek.ermittle_aenderungen(ANNA, ek.feldwerte(ANNA)), [])
|
||||
|
||||
def test_feld_leeren_ist_aenderung(self):
|
||||
"""Prüft, dass das Leeren eines Feldes als Änderung zählt."""
|
||||
self.assertEqual(ek.ermittle_aenderungen(ANNA, {"email": ""}), [("email", "anna@example.org", "")])
|
||||
|
||||
def test_wende_an(self):
|
||||
"""Prüft, dass die Änderungen im angezeigten Eintrag landen und E-Mails wieder getrennt werden; Konten bleiben."""
|
||||
neu = ek.wende_an(ANNA, [("wohnort", "Musterstadt", "Neustadt"),
|
||||
("email", "", "a@example.org, b@example.org")])
|
||||
self.assertEqual(neu.wohnort, "Neustadt")
|
||||
self.assertEqual(neu.emails, ("a@example.org", "b@example.org"))
|
||||
self.assertEqual(neu.konten, ANNA.konten)
|
||||
self.assertEqual(ANNA.wohnort, "Musterstadt")
|
||||
|
||||
|
||||
class TestKorrekturPfad(unittest.TestCase):
|
||||
|
||||
def test_textdatei_provider(self):
|
||||
"""Prüft, dass aus der Quelle eines Textdatei-Layers (file-URI, Leerzeichen kodiert) der Pfad neben der CSV wird."""
|
||||
quelle = "file:///D:/Verfahren/Eigent%C3%BCmer%20Export.csv?type=csv&detectTypes=yes&geomType=none"
|
||||
self.assertEqual(ek.korrektur_pfad(quelle), "D:/Verfahren/Eigentümer Export_korrekturen.csv")
|
||||
self.assertEqual(ek.korrektur_pfad("file:///home/test/export.CSV?type=csv"),
|
||||
"/home/test/export_korrekturen.csv")
|
||||
|
||||
def test_ogr_provider(self):
|
||||
"""Prüft, dass auch eine OGR-Quelle mit Layernamen den Pfad neben der CSV ergibt."""
|
||||
self.assertEqual(ek.korrektur_pfad("D:/Verfahren/export.csv|layername=export"),
|
||||
"D:/Verfahren/export_korrekturen.csv")
|
||||
|
||||
def test_keine_csv(self):
|
||||
"""Prüft, dass für Quellen ohne CSV-Datei kein Pfad vorgeschlagen wird."""
|
||||
self.assertIsNone(ek.korrektur_pfad(""))
|
||||
self.assertIsNone(ek.korrektur_pfad("D:/Verfahren/daten.gpkg|layername=export"))
|
||||
self.assertIsNone(ek.korrektur_pfad("memory?geometry=None"))
|
||||
|
||||
|
||||
class TestSchreiben(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._ordner = tempfile.TemporaryDirectory()
|
||||
self.pfad = os.path.join(self._ordner.name, "export_korrekturen.csv")
|
||||
|
||||
def tearDown(self):
|
||||
self._ordner.cleanup()
|
||||
|
||||
def _zeilen(self):
|
||||
with open(self.pfad, encoding="utf-8-sig", newline="") as datei:
|
||||
return list(csv.reader(datei))
|
||||
|
||||
def test_neue_datei_mit_kopfzeile_und_bom(self):
|
||||
"""Prüft, dass eine neue Korrektur-CSV mit BOM, Kopfzeile und je Änderung einer Zeile mit Exportspaltennamen entsteht."""
|
||||
anzahl = ek.schreibe_korrekturen(self.pfad, ANNA, [
|
||||
("strasse", "Hauptstraße 1", "Am Anger 3, Haus B"),
|
||||
("email", "anna@example.org", ""),
|
||||
], zeitpunkt=datetime(2026, 10, 6, 14, 30, 5))
|
||||
self.assertEqual(anzahl, 2)
|
||||
with open(self.pfad, "rb") as datei:
|
||||
self.assertTrue(datei.read().startswith(b"\xef\xbb\xbf"))
|
||||
self.assertEqual(self._zeilen(), [
|
||||
ek.KOPFZEILE,
|
||||
["2026-10-06 14:30:05", "100 200", "7", "STRASSE", "Hauptstraße 1", "Am Anger 3, Haus B"],
|
||||
["2026-10-06 14:30:05", "100 200", "7", "Email", "anna@example.org", ""],
|
||||
])
|
||||
|
||||
def test_anhaengen_ohne_zweite_kopfzeile(self):
|
||||
"""Prüft, dass weitere Korrekturen angehängt werden, ohne Kopfzeile oder BOM zu wiederholen."""
|
||||
ek.schreibe_korrekturen(self.pfad, ANNA, [("wohnort", "Musterstadt", "Neustadt")])
|
||||
ek.schreibe_korrekturen(self.pfad, ANNA, [("plz", "01234", "01235")])
|
||||
zeilen = self._zeilen()
|
||||
self.assertEqual(len(zeilen), 3)
|
||||
self.assertEqual(zeilen[0], ek.KOPFZEILE)
|
||||
self.assertEqual([z[3] for z in zeilen[1:]], ["WOHNORT", "PLZ"])
|
||||
with open(self.pfad, "rb") as datei:
|
||||
self.assertEqual(datei.read().count(b"\xef\xbb\xbf"), 1)
|
||||
|
||||
def test_ohne_aenderung_keine_datei(self):
|
||||
"""Prüft, dass ohne Änderung keine Datei angelegt wird."""
|
||||
self.assertEqual(ek.schreibe_korrekturen(self.pfad, ANNA, []), 0)
|
||||
self.assertFalse(os.path.exists(self.pfad))
|
||||
|
||||
def test_schreibfehler_wird_weitergereicht(self):
|
||||
"""Prüft, dass ein nicht schreibbarer Pfad als OSError beim Aufrufer ankommt."""
|
||||
with self.assertRaises(OSError):
|
||||
ek.schreibe_korrekturen(os.path.join(self._ordner.name, "fehlt", "x.csv"), ANNA,
|
||||
[("wohnort", "Musterstadt", "Neustadt")])
|
||||
|
||||
def test_schreibt_nichts_auf_die_konsole(self):
|
||||
"""Prüft, dass das Festhalten von Korrekturen keine Personendaten auf die Konsole schreibt (Datenschutz)."""
|
||||
ausgabe = io.StringIO()
|
||||
with contextlib.redirect_stdout(ausgabe), contextlib.redirect_stderr(ausgabe):
|
||||
ek.schreibe_korrekturen(self.pfad, ANNA, [("wohnort", "Musterstadt", "Neustadt")])
|
||||
self.assertEqual(ausgabe.getvalue(), "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Tests für die Eigentümersuche im Serienbriefexport (#56).
|
||||
|
||||
Alle Namen und Anschriften sind erfunden.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from sn_verfahrensgebiet.functions import eigentuemer_suche as es
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer
|
||||
|
||||
KOPF = [
|
||||
"EIG_KONTO", "PER_ID", "PER_VERSTORBEN", "PER_NR", "Anrede", "Titel", "Vorname", "Nachname",
|
||||
"OT", "STRASSE", "PLZ", "WOHNORT", "Email", "PER_KONTOINHABER", "PER_RECHTSPERSON",
|
||||
"EIG_GEMARKUNG", "EIG_BLATT", "LISTE_ZGBBL", "ANZAHL_PERSONEN", "EIG_ID",
|
||||
]
|
||||
|
||||
|
||||
class _Null:
|
||||
"""Nachbildung von QGIS-NULL."""
|
||||
|
||||
def isNull(self):
|
||||
return True
|
||||
|
||||
|
||||
class _Feature(dict):
|
||||
def __init__(self, fid, werte):
|
||||
super().__init__(werte)
|
||||
self._fid = fid
|
||||
|
||||
def id(self):
|
||||
return self._fid
|
||||
|
||||
|
||||
class _Layer:
|
||||
def __init__(self, zeilen, kopf=KOPF, name="Serienbrief", layer_id="csv1"):
|
||||
self._kopf = list(kopf)
|
||||
self._name = name
|
||||
self._id = layer_id
|
||||
self._features = [
|
||||
_Feature(i + 1, {spalte: zeile.get(spalte.lstrip(""), "") for spalte in self._kopf})
|
||||
for i, zeile in enumerate(zeilen)
|
||||
]
|
||||
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
def fields(self):
|
||||
return types.SimpleNamespace(names=lambda: list(self._kopf))
|
||||
|
||||
def getFeatures(self):
|
||||
return iter(self._features)
|
||||
|
||||
|
||||
def _zeile(nachname, vorname, strasse="Hauptstraße 1", plz="01234", ort="Musterstadt", **weitere):
|
||||
zeile = {"Nachname": nachname, "Vorname": vorname, "STRASSE": strasse, "PLZ": plz, "WOHNORT": ort}
|
||||
zeile.update(weitere)
|
||||
return zeile
|
||||
|
||||
|
||||
class TestLayererkennung(unittest.TestCase):
|
||||
|
||||
def test_layer_mit_name_und_vorname_ist_eigentuemerlayer(self):
|
||||
"""Prüft, dass ein Layer mit den Spalten Nachname und Vorname als Eigentümerdatei erkannt wird, auch mit BOM und anderer Schreibweise."""
|
||||
self.assertTrue(es.ist_eigentuemerlayer(_Layer([])))
|
||||
self.assertTrue(es.ist_eigentuemerlayer(_Layer([], kopf=["NACHNAME", "vorname"])))
|
||||
self.assertFalse(es.ist_eigentuemerlayer(_Layer([], kopf=["flurstueckskennzeichen"])))
|
||||
self.assertFalse(es.ist_eigentuemerlayer(object()))
|
||||
|
||||
def test_eigentuemerlayer_im_projekt(self):
|
||||
"""Prüft, dass aus dem Projekt nur passende Layer kommen, nach Namen sortiert."""
|
||||
b = _Layer([], name="b-Export", layer_id="b")
|
||||
a = _Layer([], name="A-Export", layer_id="a")
|
||||
flst = _Layer([], kopf=["flurstueckskennzeichen"], name="Flurstücke", layer_id="f")
|
||||
projekt = types.SimpleNamespace(mapLayers=lambda: {"b": b, "f": flst, "a": a})
|
||||
with patch.object(es, "QgsProject", types.SimpleNamespace(instance=lambda: projekt)):
|
||||
self.assertEqual(es.eigentuemerlayer_im_projekt(), [a, b])
|
||||
|
||||
|
||||
class TestSuche(unittest.TestCase):
|
||||
|
||||
def test_ohne_eingabe_fehler(self):
|
||||
"""Prüft, dass ohne Name und Vorname nicht die ganze Datei gelistet, sondern um eine Eingabe gebeten wird."""
|
||||
with self.assertRaisesRegex(es.EigentuemerSucheFehler, "Namen"):
|
||||
es.suche_eigentuemer(_Layer([_zeile("Beispiel", "Anna")]), " ", "")
|
||||
|
||||
def test_layer_ohne_pflichtspalten(self):
|
||||
"""Prüft, dass ein Layer ohne Nachname/Vorname mit Hinweis auf den Serienbriefexport abgelehnt wird."""
|
||||
with self.assertRaisesRegex(es.EigentuemerSucheFehler, "Serienbriefexport"):
|
||||
es.suche_eigentuemer(_Layer([], kopf=["flurstueckskennzeichen"]), "Beispiel", "")
|
||||
|
||||
def test_teilstring_ohne_gross_kleinschreibung(self):
|
||||
"""Prüft, dass Name und Vorname als Teilstring ohne Rücksicht auf Groß-/Kleinschreibung gesucht werden."""
|
||||
layer = _Layer([
|
||||
_zeile("Beispiel", "Anna"),
|
||||
_zeile("Beispiel-Muster", "Bernd"),
|
||||
_zeile("Anders", "Anna"),
|
||||
])
|
||||
self.assertEqual([e.nachname for e in es.suche_eigentuemer(layer, "beispiel", "")],
|
||||
["Beispiel", "Beispiel-Muster"])
|
||||
self.assertEqual([e.nachname for e in es.suche_eigentuemer(layer, "", "ANNA")], ["Anders", "Beispiel"])
|
||||
self.assertEqual([e.vorname for e in es.suche_eigentuemer(layer, "beisp", "an")], ["Anna"])
|
||||
|
||||
def test_umlaute_und_eszett(self):
|
||||
"""Prüft, dass „Müller“ auch „Mueller“ findet und „Strauss“ auch „Strauß“."""
|
||||
layer = _Layer([_zeile("Mueller", "Jörg"), _zeile("Strauß", "Erika", ort="Anderswo")])
|
||||
self.assertEqual(len(es.suche_eigentuemer(layer, "Müller", "Joerg")), 1)
|
||||
self.assertEqual(len(es.suche_eigentuemer(layer, "Strauss", "")), 1)
|
||||
|
||||
def test_gleiche_person_gleiche_anschrift_zusammengefasst(self):
|
||||
"""Prüft, dass Zeilen mit gleichem Namen und gleicher Anschrift ein Treffer werden und Konten, Personen-IDs und E-Mails gesammelt werden."""
|
||||
layer = _Layer([
|
||||
_zeile("Beispiel", "Anna", EIG_KONTO="100", PER_ID="7", Email="anna@example.org"),
|
||||
_zeile("beispiel ", " Anna", strasse="Hauptstraße 1", EIG_KONTO="200", PER_ID="7",
|
||||
Titel="Dr.", Email="anna@example.org"),
|
||||
_zeile("Beispiel", "Anna", EIG_KONTO="300", PER_ID="8", Email="a.b@example.org"),
|
||||
])
|
||||
treffer = es.suche_eigentuemer(layer, "Beispiel", "Anna")
|
||||
self.assertEqual(len(treffer), 1)
|
||||
e = treffer[0]
|
||||
self.assertEqual(e.konten, ("100", "200", "300"))
|
||||
self.assertEqual(e.per_ids, ("7", "8"))
|
||||
self.assertEqual(e.emails, ("anna@example.org", "a.b@example.org"))
|
||||
self.assertEqual(e.feature_ids, (1, 2, 3))
|
||||
self.assertEqual(e.titel, "Dr.")
|
||||
|
||||
def test_andere_anschrift_eigener_treffer(self):
|
||||
"""Prüft, dass dieselbe Person mit anderer Anschrift ein eigener Treffer bleibt, sortiert nach Wohnort."""
|
||||
layer = _Layer([
|
||||
_zeile("Beispiel", "Anna", ort="Zettlitz"),
|
||||
_zeile("Beispiel", "Anna", ort="Altdorf", plz="09999"),
|
||||
])
|
||||
treffer = es.suche_eigentuemer(layer, "Beispiel", "Anna")
|
||||
self.assertEqual([e.wohnort for e in treffer], ["Altdorf", "Zettlitz"])
|
||||
|
||||
def test_kennzeichen_verstorben(self):
|
||||
"""Prüft, dass „verstorben“ beim Exportwert 1 (auch als Zahl gelesen) gesetzt ist und bei 0, Leerem und NULL nicht."""
|
||||
ja = ["1", 1]
|
||||
nein = ["0", 0, "", None, _Null()]
|
||||
zeilen = [_zeile(f"Ja{i}", "Test", PER_VERSTORBEN=w) for i, w in enumerate(ja)]
|
||||
zeilen += [_zeile(f"Nein{i}", "Test", PER_VERSTORBEN=w) for i, w in enumerate(nein)]
|
||||
for e in es.suche_eigentuemer(_Layer(zeilen), "", "Test"):
|
||||
erwartet = e.nachname.startswith("Ja")
|
||||
with self.subTest(name=e.nachname):
|
||||
self.assertEqual(e.verstorben, erwartet)
|
||||
self.assertEqual(e.hinweise, "verstorben" if erwartet else "")
|
||||
|
||||
def test_rechtsperson_als_rohwert(self):
|
||||
"""Prüft, dass PER_RECHTSPERSON unverändert angezeigt wird (0, 1, 5, 9; noch nicht entschlüsselt) und abweichende Werte einer Person gesammelt werden."""
|
||||
layer = _Layer([
|
||||
_zeile("Beispiel", "Anna", PER_RECHTSPERSON="0"),
|
||||
_zeile("Gemeinde Musterstadt", "", strasse="Markt 1", PER_RECHTSPERSON=5),
|
||||
_zeile("Muster", "Bernd", PER_RECHTSPERSON="1"),
|
||||
_zeile("Muster", "Bernd", PER_RECHTSPERSON="9"),
|
||||
_zeile("Ohnewert", "Clara", PER_RECHTSPERSON=_Null()),
|
||||
])
|
||||
werte = {e.nachname: e.rechtsperson_text for e in es.suche_eigentuemer(layer, "e", "")}
|
||||
self.assertEqual(werte, {"Beispiel": "0", "Gemeinde Musterstadt": "5", "Muster": "1, 9", "Ohnewert": ""})
|
||||
|
||||
def test_plz_mit_fuehrender_null(self):
|
||||
"""Prüft, dass eine von QGIS als Zahl gelesene PLZ wieder fünfstellig mit führender Null erscheint und Zeilen dadurch zusammenfinden."""
|
||||
layer = _Layer([
|
||||
_zeile("Beispiel", "Anna", plz=1067),
|
||||
_zeile("Beispiel", "Anna", plz="01067"),
|
||||
_zeile("Muster", "Bernd", plz="1067.0"),
|
||||
])
|
||||
treffer = es.suche_eigentuemer(layer, "e", "")
|
||||
self.assertEqual([(e.nachname, e.plz) for e in treffer], [("Beispiel", "01067"), ("Muster", "01067")])
|
||||
|
||||
def test_suche_gibt_nichts_aus(self):
|
||||
"""Prüft, dass die Suche keine Personendaten auf die Konsole schreibt (Datenschutz)."""
|
||||
ausgabe = io.StringIO()
|
||||
with contextlib.redirect_stdout(ausgabe), contextlib.redirect_stderr(ausgabe):
|
||||
es.suche_eigentuemer(_Layer([_zeile("Beispiel", "Anna")]), "Beispiel", "")
|
||||
self.assertEqual(ausgabe.getvalue(), "")
|
||||
|
||||
|
||||
class TestAnzeigeformen(unittest.TestCase):
|
||||
|
||||
def test_name_und_anschrift(self):
|
||||
"""Prüft, dass Name mit Titel und einzeilige Anschrift mit Ortsteil lesbar zusammengesetzt werden."""
|
||||
e = Eigentuemer(titel="Dr.", vorname="Anna", nachname="Beispiel", strasse="Hauptstraße 1",
|
||||
plz="01234", wohnort="Musterstadt", ortsteil="Unterdorf")
|
||||
self.assertEqual(e.name, "Beispiel, Dr. Anna")
|
||||
self.assertEqual(e.anschrift, "Hauptstraße 1, 01234 Musterstadt, OT Unterdorf")
|
||||
self.assertEqual(Eigentuemer(nachname="Beispiel", ortsteil="OT Unterdorf").anschrift, "OT Unterdorf")
|
||||
self.assertEqual(Eigentuemer(nachname="Gemeinde Musterstadt").name, "Gemeinde Musterstadt")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,433 @@
|
||||
"""Tests für den Tab „Eigentümer“ – Flurstückssuche, Eigentümersuche, Anzeige und Korrektur (#56).
|
||||
|
||||
Alle Namen und Anschriften sind erfunden.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
if "qgis" not in sys.modules:
|
||||
qgis_mod = types.ModuleType("qgis")
|
||||
qgis_utils = types.ModuleType("qgis.utils")
|
||||
qgis_utils.iface = types.SimpleNamespace()
|
||||
qgis_utils.plugins = {}
|
||||
qgis_mod.utils = qgis_utils
|
||||
sys.modules["qgis"] = qgis_mod
|
||||
sys.modules["qgis.utils"] = qgis_utils
|
||||
|
||||
from sn_verfahrensgebiet.ui.tabs import eigentuemer_tab as et
|
||||
from sn_verfahrensgebiet.ui.tabs.eigentuemer_tab import EigentuemerTab
|
||||
from sn_verfahrensgebiet.functions.flurstueck_suche import (
|
||||
FlurstueckSucheFehler,
|
||||
KeinFlurstueckslayer,
|
||||
SuchErgebnis,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer, EigentuemerSucheFehler
|
||||
from sn_verfahrensgebiet.ui import eigentuemer_treffer as treffer_modul
|
||||
from sn_verfahrensgebiet.ui.eigentuemer_treffer import EigentuemerTrefferFenster, trefferzeile
|
||||
|
||||
|
||||
def _tab(gemarkung="0268", flurstueck="562"):
|
||||
tab = EigentuemerTab.__new__(EigentuemerTab)
|
||||
tab.gemarkung_edit = types.SimpleNamespace(text=lambda: gemarkung)
|
||||
tab.flurstueck_edit = types.SimpleNamespace(text=lambda: flurstueck)
|
||||
return tab
|
||||
|
||||
|
||||
@patch.object(et, "success")
|
||||
@patch.object(et, "info")
|
||||
@patch.object(et, "warning")
|
||||
@patch.object(et, "show_info_dialog")
|
||||
class TestFlurstueckSuchen(unittest.TestCase):
|
||||
|
||||
def test_ohne_flurstueckslayer_erscheint_hinweisdialog(self, dialog, warnung, hinweis, erfolg):
|
||||
"""Prüft, dass ohne Layer „Flurstücke“ ein Dialog zum Laden von Verfahrensgebiet und Flurstücken erscheint."""
|
||||
with patch.object(et, "flurstueck_suchen", side_effect=KeinFlurstueckslayer()):
|
||||
_tab()._on_flurstueck_suchen()
|
||||
dialog.assert_called_once()
|
||||
self.assertIn("Flurstücke aus ALKIS laden", dialog.call_args[0][1])
|
||||
warnung.assert_not_called()
|
||||
erfolg.assert_not_called()
|
||||
|
||||
def test_eingabefehler_als_warnung(self, dialog, warnung, hinweis, erfolg):
|
||||
"""Prüft, dass eine ungültige Eingabe als Warnung in der Meldungsleiste erscheint, nicht als Dialog."""
|
||||
with patch.object(et, "flurstueck_suchen", side_effect=FlurstueckSucheFehler("falsch")):
|
||||
_tab()._on_flurstueck_suchen()
|
||||
warnung.assert_called_once_with("Flurstückssuche", "falsch")
|
||||
dialog.assert_not_called()
|
||||
|
||||
def test_kein_treffer(self, dialog, warnung, hinweis, erfolg):
|
||||
"""Prüft, dass ohne Treffer ein Hinweis erscheint und keine Auswahl gesetzt wird."""
|
||||
layer = MagicMock()
|
||||
with patch.object(et, "flurstueck_suchen", return_value=SuchErgebnis(layer, [])):
|
||||
_tab(flurstueck="999")._on_flurstueck_suchen()
|
||||
hinweis.assert_called_once()
|
||||
self.assertIn("999", hinweis.call_args[0][1])
|
||||
layer.selectByIds.assert_not_called()
|
||||
|
||||
def test_treffer_werden_ausgewaehlt_und_angezoomt(self, dialog, warnung, hinweis, erfolg):
|
||||
"""Prüft, dass Treffer im Flurstückslayer ausgewählt, angezoomt und gemeldet werden."""
|
||||
layer = MagicMock()
|
||||
layer.name.return_value = "Flurstücke"
|
||||
canvas = MagicMock()
|
||||
with patch.object(et, "flurstueck_suchen", return_value=SuchErgebnis(layer, [1, 2])) as suche, \
|
||||
patch.object(et, "iface", types.SimpleNamespace(mapCanvas=lambda: canvas)):
|
||||
_tab(gemarkung=" Beispielhausen ", flurstueck=" 562 ")._on_flurstueck_suchen()
|
||||
suche.assert_called_once_with("Beispielhausen", "562")
|
||||
layer.selectByIds.assert_called_once_with([1, 2])
|
||||
canvas.zoomToSelected.assert_called_once_with(layer)
|
||||
canvas.flashFeatureIds.assert_called_once_with(layer, [1, 2])
|
||||
erfolg.assert_called_once()
|
||||
self.assertIn("2 Flurstücke", erfolg.call_args[0][1])
|
||||
|
||||
def test_ohne_kartenansicht_bleibt_auswahl(self, dialog, warnung, hinweis, erfolg):
|
||||
"""Prüft, dass die Auswahl auch ohne Kartenansicht gesetzt und gemeldet wird."""
|
||||
layer = MagicMock()
|
||||
with patch.object(et, "flurstueck_suchen", return_value=SuchErgebnis(layer, [7])), \
|
||||
patch.object(et, "iface", types.SimpleNamespace()):
|
||||
_tab()._on_flurstueck_suchen()
|
||||
layer.selectByIds.assert_called_once_with([7])
|
||||
self.assertIn("1 Flurstück", erfolg.call_args[0][1])
|
||||
|
||||
|
||||
def _eig_tab(nachname="Beispiel", vorname="", layer_id="csv1"):
|
||||
tab = EigentuemerTab.__new__(EigentuemerTab)
|
||||
tab.nachname_edit = types.SimpleNamespace(text=lambda: nachname)
|
||||
tab.vorname_edit = types.SimpleNamespace(text=lambda: vorname)
|
||||
tab.eigentuemer_layer_combo = MagicMock()
|
||||
tab.eigentuemer_layer_combo.currentData.return_value = layer_id
|
||||
tab._trefferfenster = None
|
||||
return tab
|
||||
|
||||
|
||||
_ANNA = Eigentuemer(vorname="Anna", nachname="Beispiel", strasse="Hauptstraße 1", plz="01234",
|
||||
wohnort="Musterstadt", emails=("anna@example.org",), verstorben=True,
|
||||
rechtsperson=("0",))
|
||||
|
||||
|
||||
@patch.object(et, "info")
|
||||
@patch.object(et, "warning")
|
||||
class TestEigentuemerSuchen(unittest.TestCase):
|
||||
|
||||
def test_ohne_eigentuemerdatei_warnung(self, warnung, hinweis):
|
||||
"""Prüft, dass ohne gewählte Eigentümerdatei die Liste aufgefrischt und dann auf das Laden der CSV hingewiesen wird."""
|
||||
tab = _eig_tab(layer_id=None)
|
||||
with patch.object(et, "hole_layer", return_value=None), \
|
||||
patch.object(et, "suche_eigentuemer") as suche:
|
||||
tab._on_eigentuemer_suchen()
|
||||
tab.eigentuemer_layer_combo.aktualisieren.assert_called_once()
|
||||
warnung.assert_called_once()
|
||||
self.assertIn("CSV", warnung.call_args[0][1])
|
||||
suche.assert_not_called()
|
||||
|
||||
def test_suchfehler_als_warnung(self, warnung, hinweis):
|
||||
"""Prüft, dass ein Fehler der Suche (z. B. leere Eingabe) als Warnung erscheint."""
|
||||
with patch.object(et, "hole_layer", return_value=object()), \
|
||||
patch.object(et, "suche_eigentuemer", side_effect=EigentuemerSucheFehler("Bitte eingeben")):
|
||||
_eig_tab()._on_eigentuemer_suchen()
|
||||
warnung.assert_called_once_with("Eigentümersuche", "Bitte eingeben")
|
||||
|
||||
def test_kein_treffer_ohne_namen_in_meldung(self, warnung, hinweis):
|
||||
"""Prüft, dass ohne Treffer ein Hinweis erscheint, der den gesuchten Namen nicht wiederholt (Datenschutz)."""
|
||||
with patch.object(et, "hole_layer", return_value=object()), \
|
||||
patch.object(et, "suche_eigentuemer", return_value=[]), \
|
||||
patch.object(et, "EigentuemerTrefferFenster") as fenster:
|
||||
_eig_tab(nachname="Beispiel")._on_eigentuemer_suchen()
|
||||
hinweis.assert_called_once()
|
||||
self.assertNotIn("Beispiel", hinweis.call_args[0][1])
|
||||
fenster.assert_not_called()
|
||||
|
||||
def test_treffer_oeffnen_trefferliste_und_ersetzen_die_alte(self, warnung, hinweis):
|
||||
"""Prüft, dass Treffer in einer neuen Trefferliste mit Anzeige-Rückruf erscheinen, eine offene alte Liste geschlossen wird und die Eingaben an die Suche gehen."""
|
||||
layer = MagicMock()
|
||||
layer.id.return_value = "csv1"
|
||||
tab = _eig_tab(nachname="Beisp", vorname="An")
|
||||
altes_fenster = MagicMock()
|
||||
tab._trefferfenster = altes_fenster
|
||||
with patch.object(et, "hole_layer", return_value=layer), \
|
||||
patch.object(et, "suche_eigentuemer", return_value=[_ANNA]) as suche, patch.object(et, "EigentuemerTrefferFenster") as fenster:
|
||||
tab._on_eigentuemer_suchen()
|
||||
suche.assert_called_once_with(layer, "Beisp", "An")
|
||||
altes_fenster.close.assert_called_once()
|
||||
fenster.assert_called_once_with([_ANNA], tab, beim_anzeigen=tab._daten_anzeigen)
|
||||
fenster.return_value.show.assert_called_once()
|
||||
self.assertIs(tab._trefferfenster, fenster.return_value)
|
||||
self.assertEqual(tab._treffer_layer_id, "csv1")
|
||||
|
||||
|
||||
class TestTrefferliste(unittest.TestCase):
|
||||
|
||||
def test_trefferzeile_mit_hinweis(self):
|
||||
"""Prüft, dass die Trefferliste Name, Anschrift, E-Mail, den Rohwert von PER_RECHTSPERSON und das Kennzeichen „verstorben“ zeigt."""
|
||||
self.assertEqual(
|
||||
trefferzeile(_ANNA),
|
||||
("Beispiel, Anna", "Hauptstraße 1, 01234 Musterstadt", "anna@example.org", "0", "verstorben"),
|
||||
)
|
||||
|
||||
def test_ausgewaehlte_eigentuemer(self):
|
||||
"""Prüft, dass das Trefferfenster die in der Tabelle gewählten Einträge zurückgibt."""
|
||||
bernd = Eigentuemer(vorname="Bernd", nachname="Muster")
|
||||
fenster = EigentuemerTrefferFenster.__new__(EigentuemerTrefferFenster)
|
||||
fenster.treffer = [_ANNA, bernd]
|
||||
fenster.tabelle = object()
|
||||
with patch.object(treffer_modul, "ausgewaehlte_indizes", return_value=[1]) as indizes:
|
||||
self.assertEqual(fenster.ausgewaehlte_eigentuemer(), [bernd])
|
||||
indizes.assert_called_once_with(fenster.tabelle)
|
||||
|
||||
def test_daten_anzeigen_gibt_auswahl_weiter(self):
|
||||
"""Prüft, dass „Daten anzeigen“ die Auswahl der Trefferliste an den Tab übergibt."""
|
||||
fenster = EigentuemerTrefferFenster.__new__(EigentuemerTrefferFenster)
|
||||
fenster.treffer = [_ANNA]
|
||||
fenster.tabelle = object()
|
||||
fenster.beim_anzeigen = MagicMock()
|
||||
with patch.object(treffer_modul, "ausgewaehlte_indizes", return_value=[0]):
|
||||
fenster._on_anzeigen()
|
||||
fenster.beim_anzeigen.assert_called_once_with([_ANNA])
|
||||
|
||||
|
||||
def _anz_tab(angezeigt=None, anzeige=None, layer_id="csv1"):
|
||||
tab = EigentuemerTab.__new__(EigentuemerTab)
|
||||
for name in ("btn_sitzung", "btn_anzeige_leeren", "btn_korrigieren", "sitzung_label", "korrektur_label",
|
||||
"eigentuemer_layer_combo"):
|
||||
setattr(tab, name, MagicMock())
|
||||
tab.eigentuemer_layer_combo.currentData.return_value = layer_id
|
||||
tab._anzeige = anzeige
|
||||
tab._anzeige_geometrie = None
|
||||
tab._sitzung_seit = datetime(2026, 10, 6, 9, 5) if anzeige is not None else None
|
||||
tab._angezeigt = list(angezeigt or [])
|
||||
tab._treffer_layer_id = layer_id
|
||||
tab._anzeige_layer_id = layer_id
|
||||
tab._korrektur_pfade = {}
|
||||
return tab
|
||||
|
||||
|
||||
_BERND = Eigentuemer(vorname="Bernd", nachname="Muster", konten=("300",), per_ids=("8",))
|
||||
|
||||
|
||||
@patch.object(et, "hole_layer", return_value=None)
|
||||
@patch.object(et, "zeige_anzeigefenster")
|
||||
@patch.object(et, "EigentuemerAnzeige")
|
||||
class TestSitzung(unittest.TestCase):
|
||||
|
||||
def test_sitzung_beginnen_oeffnet_leeres_fenster_mit_rahmen(self, anzeige_klasse, zeige, _layer):
|
||||
"""Prüft, dass „Sitzung beginnen“ ein leeres Anzeigefenster als normales Fenster öffnet (ohne gemerkte Lage beim ersten Mal)."""
|
||||
tab = _anz_tab()
|
||||
tab._on_sitzung_umschalten()
|
||||
anzeige_klasse.assert_called_once_with(tab)
|
||||
zeige.assert_called_once_with(anzeige_klasse.return_value, None)
|
||||
self.assertIs(tab._anzeige, anzeige_klasse.return_value)
|
||||
self.assertEqual(tab._angezeigt, [])
|
||||
tab.btn_sitzung.setText.assert_called_with("Sitzung beenden")
|
||||
self.assertIn("leer", tab.sitzung_label.setText.call_args[0][0])
|
||||
|
||||
def test_lage_gilt_fuer_naechste_sitzung(self, anzeige_klasse, zeige, _layer):
|
||||
"""Prüft, dass Größe und Lage des Fensters beim Beenden gemerkt und in der nächsten Sitzung wiederverwendet werden."""
|
||||
tab = _anz_tab()
|
||||
tab._sitzung_beginnen()
|
||||
anzeige_klasse.return_value.saveGeometry.return_value = b"zweiter-bildschirm"
|
||||
tab._sitzung_beenden()
|
||||
tab._sitzung_beginnen()
|
||||
self.assertEqual(zeige.call_args_list[-1][0][1], b"zweiter-bildschirm")
|
||||
|
||||
def test_daten_anzeigen_beginnt_sitzung_bei_bedarf(self, anzeige_klasse, zeige, _layer):
|
||||
"""Prüft, dass „Daten anzeigen“ ohne laufende Sitzung diese beginnt und die Einträge zeigt; die Statuszeile nennt nur die Anzahl."""
|
||||
tab = _anz_tab()
|
||||
tab._daten_anzeigen([_ANNA, _BERND])
|
||||
anzeige_klasse.return_value.zeige.assert_called_once_with([_ANNA, _BERND])
|
||||
self.assertEqual(tab._angezeigt, [_ANNA, _BERND])
|
||||
status = tab.sitzung_label.setText.call_args[0][0]
|
||||
self.assertIn("2 Einträge", status)
|
||||
self.assertNotIn("Beispiel", status)
|
||||
|
||||
def test_daten_anzeigen_ohne_auswahl(self, anzeige_klasse, zeige, _layer):
|
||||
"""Prüft, dass „Daten anzeigen“ ohne Auswahl nur einen Hinweis gibt und keine Sitzung beginnt."""
|
||||
tab = _anz_tab()
|
||||
with patch.object(et, "warning") as warnung:
|
||||
tab._daten_anzeigen([])
|
||||
warnung.assert_called_once()
|
||||
anzeige_klasse.assert_not_called()
|
||||
|
||||
def test_anzeige_leeren_laesst_fenster_offen(self, anzeige_klasse, zeige, _layer):
|
||||
"""Prüft, dass „Anzeige leeren“ die Daten entfernt, das Fenster aber nicht schließt."""
|
||||
anzeige = MagicMock()
|
||||
tab = _anz_tab(angezeigt=[_ANNA], anzeige=anzeige)
|
||||
tab._on_anzeige_leeren()
|
||||
anzeige.leeren.assert_called_once()
|
||||
anzeige.close.assert_not_called()
|
||||
self.assertEqual(tab._angezeigt, [])
|
||||
self.assertIs(tab._anzeige, anzeige)
|
||||
tab.btn_anzeige_leeren.setEnabled.assert_called_with(False)
|
||||
|
||||
def test_sitzung_beenden_schliesst_fenster(self, anzeige_klasse, zeige, _layer):
|
||||
"""Prüft, dass „Sitzung beenden“ das Anzeigefenster schließt und die angezeigten Daten vergisst."""
|
||||
anzeige = MagicMock()
|
||||
tab = _anz_tab(angezeigt=[_ANNA], anzeige=anzeige)
|
||||
tab._on_sitzung_umschalten()
|
||||
anzeige.close.assert_called_once()
|
||||
self.assertIsNone(anzeige.beim_schliessen)
|
||||
self.assertIsNone(tab._anzeige)
|
||||
self.assertEqual(tab._angezeigt, [])
|
||||
tab.btn_sitzung.setText.assert_called_with("Sitzung beginnen")
|
||||
|
||||
def test_von_aussen_geschlossen_beendet_sitzung(self, anzeige_klasse, zeige, _layer):
|
||||
"""Prüft, dass ein über den Fensterrahmen geschlossenes Anzeigefenster die Sitzung beendet und seine Lage gemerkt wird."""
|
||||
tab = _anz_tab()
|
||||
tab._sitzung_beginnen()
|
||||
anzeige_klasse.return_value.saveGeometry.return_value = b"lage"
|
||||
anzeige_klasse.return_value.beim_schliessen()
|
||||
self.assertIsNone(tab._anzeige)
|
||||
self.assertEqual(tab._anzeige_geometrie, b"lage")
|
||||
|
||||
|
||||
class TestKorrekturLabel(unittest.TestCase):
|
||||
|
||||
def _text(self, tab):
|
||||
return tab.korrektur_label.setText.call_args[0][0]
|
||||
|
||||
def test_ohne_eigentuemerdatei(self):
|
||||
"""Prüft, dass ohne Eigentümerdatei unter den Knöpfen steht, dass keine gewählt ist."""
|
||||
tab = _anz_tab(layer_id=None)
|
||||
tab._aktualisiere_korrektur_label()
|
||||
self.assertIn("keine Eigentümerdatei", self._text(tab))
|
||||
|
||||
def test_pfad_neben_der_eigentuemerdatei(self):
|
||||
"""Prüft, dass der Pfad der Korrektur-CSV angezeigt wird, mit Hinweis, solange sie noch nicht angelegt ist."""
|
||||
with tempfile.TemporaryDirectory() as ordner:
|
||||
quelle = "file:///" + os.path.join(ordner, "export.csv").replace(os.sep, "/") + "?type=csv"
|
||||
erwartet = os.path.normpath(os.path.join(ordner, "export_korrekturen.csv"))
|
||||
tab = _anz_tab()
|
||||
with patch.object(et, "hole_layer", return_value=types.SimpleNamespace(source=lambda: quelle)):
|
||||
tab._aktualisiere_korrektur_label()
|
||||
self.assertEqual(os.path.normcase(self._text(tab)),
|
||||
os.path.normcase(f"Korrektur-Datei: {erwartet} (wird beim ersten Speichern angelegt)"))
|
||||
open(erwartet, "w").close()
|
||||
tab._aktualisiere_korrektur_label()
|
||||
self.assertEqual(os.path.normcase(self._text(tab)), os.path.normcase(f"Korrektur-Datei: {erwartet}"))
|
||||
|
||||
def test_ohne_csv_quelle_wird_abgefragt(self):
|
||||
"""Prüft, dass bei einer Quelle ohne CSV angekündigt wird, dass der Speicherort beim ersten Speichern abgefragt wird."""
|
||||
tab = _anz_tab()
|
||||
with patch.object(et, "hole_layer", return_value=types.SimpleNamespace(source=lambda: "memory?geometry=None")):
|
||||
tab._aktualisiere_korrektur_label()
|
||||
self.assertIn("abgefragt", self._text(tab))
|
||||
|
||||
def test_gewaehlter_pfad_wird_angezeigt(self):
|
||||
"""Prüft, dass ein einmal gewählter Speicherort angezeigt wird."""
|
||||
tab = _anz_tab()
|
||||
tab._korrektur_pfade["csv1"] = "D:/Termine/korrekturen.csv"
|
||||
with patch.object(et, "hole_layer", return_value=None):
|
||||
tab._aktualisiere_korrektur_label()
|
||||
self.assertIn(os.path.normpath("D:/Termine/korrekturen.csv"), self._text(tab))
|
||||
|
||||
|
||||
@patch.object(et, "hole_layer", return_value=None)
|
||||
@patch.object(et, "success")
|
||||
@patch.object(et, "info")
|
||||
@patch.object(et, "warning")
|
||||
class TestKorrektur(unittest.TestCase):
|
||||
|
||||
def test_korrektur_wird_festgehalten_und_angezeigt(self, warnung, hinweis, erfolg, _layer):
|
||||
"""Prüft, dass eine Korrektur in die Korrektur-CSV geschrieben, sofort angezeigt und ohne Namen gemeldet wird."""
|
||||
anzeige = MagicMock()
|
||||
tab = _anz_tab(angezeigt=[_ANNA, _BERND], anzeige=anzeige)
|
||||
tab._korrektur_pfade["csv1"] = "D:/Verfahren/export_korrekturen.csv"
|
||||
with patch.object(et, "frage_korrektur", return_value=(1, {"wohnort": "Neustadt"})), \
|
||||
patch.object(et, "schreibe_korrekturen") as schreiben:
|
||||
tab._on_korrigieren()
|
||||
schreiben.assert_called_once_with("D:/Verfahren/export_korrekturen.csv", _BERND,
|
||||
[("wohnort", "", "Neustadt")])
|
||||
self.assertEqual(tab._angezeigt[1].wohnort, "Neustadt")
|
||||
self.assertIs(tab._angezeigt[0], _ANNA)
|
||||
anzeige.zeige.assert_called_once_with(tab._angezeigt)
|
||||
meldung = erfolg.call_args[0][1]
|
||||
self.assertIn("export_korrekturen.csv", meldung)
|
||||
self.assertNotIn("Muster", meldung)
|
||||
|
||||
def test_ohne_aenderung_nichts_schreiben(self, warnung, hinweis, erfolg, _layer):
|
||||
"""Prüft, dass ein unverändert gespeicherter Dialog nichts schreibt."""
|
||||
tab = _anz_tab(angezeigt=[_ANNA], anzeige=MagicMock())
|
||||
with patch.object(et, "frage_korrektur", return_value=(0, {"nachname": "Beispiel"})), \
|
||||
patch.object(et, "schreibe_korrekturen") as schreiben:
|
||||
tab._on_korrigieren()
|
||||
schreiben.assert_not_called()
|
||||
hinweis.assert_called_once()
|
||||
|
||||
def test_abbruch_nichts_schreiben(self, warnung, hinweis, erfolg, _layer):
|
||||
"""Prüft, dass ein abgebrochener Korrekturdialog nichts schreibt."""
|
||||
tab = _anz_tab(angezeigt=[_ANNA], anzeige=MagicMock())
|
||||
with patch.object(et, "frage_korrektur", return_value=None), \
|
||||
patch.object(et, "schreibe_korrekturen") as schreiben:
|
||||
tab._on_korrigieren()
|
||||
schreiben.assert_not_called()
|
||||
|
||||
def test_schreibfehler_warnung_und_anzeige_unveraendert(self, warnung, hinweis, erfolg, _layer):
|
||||
"""Prüft, dass bei einem Schreibfehler gewarnt, die Anzeige nicht geändert und der Pfad beim nächsten Mal neu bestimmt wird."""
|
||||
anzeige = MagicMock()
|
||||
tab = _anz_tab(angezeigt=[_ANNA], anzeige=anzeige)
|
||||
tab._korrektur_pfade["csv1"] = "X:/gesperrt.csv"
|
||||
with patch.object(et, "frage_korrektur", return_value=(0, {"wohnort": "Neustadt"})), \
|
||||
patch.object(et, "schreibe_korrekturen", side_effect=PermissionError(13, "Zugriff verweigert")):
|
||||
tab._on_korrigieren()
|
||||
warnung.assert_called_once()
|
||||
anzeige.zeige.assert_not_called()
|
||||
self.assertIs(tab._angezeigt[0], _ANNA)
|
||||
self.assertNotIn("csv1", tab._korrektur_pfade)
|
||||
|
||||
def test_ohne_anzeige_keine_korrektur(self, warnung, hinweis, erfolg, _layer):
|
||||
"""Prüft, dass ohne angezeigte Daten kein Korrekturdialog erscheint."""
|
||||
tab = _anz_tab()
|
||||
with patch.object(et, "frage_korrektur") as dialog:
|
||||
tab._on_korrigieren()
|
||||
dialog.assert_not_called()
|
||||
warnung.assert_called_once()
|
||||
|
||||
|
||||
class TestKorrekturDatei(unittest.TestCase):
|
||||
|
||||
def test_vorschlag_neben_der_eigentuemerdatei(self):
|
||||
"""Prüft, dass die Korrektur-CSV ohne Rückfrage neben der Eigentümerdatei liegt, wenn der Ordner beschreibbar ist, und gemerkt wird."""
|
||||
with tempfile.TemporaryDirectory() as ordner:
|
||||
quelle = "file:///" + os.path.join(ordner, "export.csv").replace(os.sep, "/") + "?type=csv"
|
||||
layer = types.SimpleNamespace(source=lambda: quelle)
|
||||
tab = _anz_tab()
|
||||
with patch.object(et, "hole_layer", return_value=layer), \
|
||||
patch.object(et.QFileDialog, "getSaveFileName") as dialog:
|
||||
pfad = tab._korrektur_datei()
|
||||
dialog.assert_not_called()
|
||||
self.assertEqual(os.path.normcase(os.path.normpath(pfad)),
|
||||
os.path.normcase(os.path.join(ordner, "export_korrekturen.csv")))
|
||||
self.assertEqual(tab._korrektur_pfade["csv1"], pfad)
|
||||
|
||||
def test_ohne_csv_quelle_wird_gefragt(self):
|
||||
"""Prüft, dass ohne verwertbare CSV-Quelle nach dem Speicherort gefragt wird; Abbrechen liefert keinen Pfad."""
|
||||
tab = _anz_tab()
|
||||
layer = types.SimpleNamespace(source=lambda: "memory?geometry=None")
|
||||
with patch.object(et, "hole_layer", return_value=layer), \
|
||||
patch.object(et.QFileDialog, "getSaveFileName", return_value=("", "")) as dialog:
|
||||
self.assertIsNone(tab._korrektur_datei())
|
||||
dialog.assert_called_once()
|
||||
self.assertEqual(tab._korrektur_pfade, {})
|
||||
|
||||
with patch.object(et, "hole_layer", return_value=layer), \
|
||||
patch.object(et.QFileDialog, "getSaveFileName", return_value=("D:/k.csv", "")):
|
||||
self.assertEqual(tab._korrektur_datei(), "D:/k.csv")
|
||||
|
||||
|
||||
class TestDockWidgetTabs(unittest.TestCase):
|
||||
|
||||
def test_tab_ist_im_dockwidget_eingetragen(self):
|
||||
"""Prüft, dass der Tab „Eigentümer“ im DockWidget direkt nach dem Bearbeitungs-Tab steht."""
|
||||
from sn_verfahrensgebiet.ui.dockwidget import DockWidget
|
||||
from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab
|
||||
|
||||
self.assertEqual(DockWidget.tabs[:2], [WorkingTab, EigentuemerTab])
|
||||
self.assertEqual(EigentuemerTab.tab_title, "Eigentümer")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+221
-86
@@ -6,8 +6,11 @@ Getestet werden:
|
||||
- Nutzerabbruch bei ask_text → (None, LoadStatus.NONE)
|
||||
- Ungültiger WFS-Layer → (None, LoadStatus.NONE)
|
||||
- Erfolgreicher Ladevorgang → gültiger Layer + LoadStatus.FIRST
|
||||
- Bestehender Layer: Nutzer wählt "Neu laden" → LoadStatus.RELOAD
|
||||
- Bestehender Layer: Nutzer wählt "Behalten" → LoadStatus.KEEP
|
||||
- Bestehender Layer, Drei-Knopf-Dialog: "Überschreiben" → LoadStatus.RELOAD,
|
||||
"Abbrechen" → (None, LoadStatus.NONE), "Anhängen" siehe
|
||||
TestFlurstueckAlkisAnhaengenDedup
|
||||
- vorgabe_schluessel/vorgabe_namen füllen den Eingabedialog vor
|
||||
- ohne_schluessel_abfrage=True überspringt die ask_text-Abfrage
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -30,7 +33,12 @@ if "qgis" not in sys.modules:
|
||||
sys.modules["qgis.core"] = qgis_core
|
||||
sys.modules["qgis.utils"] = qgis_utils
|
||||
|
||||
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis, FLST_LAYER_NAME
|
||||
from sn_verfahrensgebiet.functions.flurstueck_alkis import (
|
||||
flurstueck_alkis,
|
||||
lade_flurstuecke_per_gemarkung,
|
||||
FLST_LAYER_NAME,
|
||||
FELD_FLURSTUECKSKENNZEICHEN,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
|
||||
|
||||
|
||||
@@ -49,8 +57,8 @@ class _DummyLayer:
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
def extent(self):
|
||||
return object()
|
||||
def featureCount(self):
|
||||
return 1
|
||||
|
||||
|
||||
class _DummyProject:
|
||||
@@ -78,7 +86,7 @@ class TestFlurstueckAlkis(unittest.TestCase):
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_user_cancel_returns_none(self, mock_ask_text, mock_project, mock_msgbox):
|
||||
"""ask_text → ok=False, text="" → (None, NONE)."""
|
||||
"""Prüft, dass ein Abbruch im Eingabedialog (ok=False) (None, LoadStatus.NONE) liefert."""
|
||||
mock_ask_text.return_value = ("", False)
|
||||
mock_project.return_value = _DummyProject()
|
||||
|
||||
@@ -91,7 +99,7 @@ class TestFlurstueckAlkis(unittest.TestCase):
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_empty_text_after_confirmation_returns_none(self, mock_ask_text, mock_project, mock_msgbox):
|
||||
"""ask_text → ok=True, aber Text leer → (None, NONE)."""
|
||||
"""Prüft, dass ein bestätigter, aber leerer Eingabetext ebenfalls (None, LoadStatus.NONE) liefert."""
|
||||
mock_ask_text.return_value = (" ", True)
|
||||
mock_project.return_value = _DummyProject()
|
||||
|
||||
@@ -110,7 +118,7 @@ class TestFlurstueckAlkis(unittest.TestCase):
|
||||
def test_invalid_wfs_layer_returns_none(
|
||||
self, mock_ask_text, mock_project, mock_wfs_layer_cls, mock_msgbox
|
||||
):
|
||||
"""WFS-Layer nicht valide → (None, NONE)."""
|
||||
"""Prüft, dass ein ungültiger WFS-Layer (None, LoadStatus.NONE) liefert."""
|
||||
mock_ask_text.return_value = ("1234", True)
|
||||
mock_project.return_value = _DummyProject()
|
||||
mock_wfs_layer_cls.return_value = _DummyLayer(valid=False)
|
||||
@@ -123,67 +131,64 @@ class TestFlurstueckAlkis(unittest.TestCase):
|
||||
# ------------------------------------------------------------------ #
|
||||
# 3. Erfolgreicher Erstladevorgang #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_success_returns_layer_and_first_status(
|
||||
self, mock_ask_text, mock_project, mock_wfs_layer_cls
|
||||
self, mock_ask_text, mock_project, mock_lade
|
||||
):
|
||||
"""Kein bestehender Layer, WFS valide → (layer, FIRST)."""
|
||||
"""Prüft, dass ohne bestehenden Layer der von lade_flurstuecke_per_gemarkung gelieferte Layer mit LoadStatus.FIRST zurückgegeben wird."""
|
||||
mock_ask_text.return_value = ("1234, 5678", True)
|
||||
mock_project.return_value = _DummyProject()
|
||||
valid_layer = _DummyLayer(valid=True, layer_id="wfs_ok")
|
||||
mock_wfs_layer_cls.return_value = valid_layer
|
||||
mock_lade.return_value = valid_layer
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
self.assertIs(layer, valid_layer)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
mock_lade.assert_called_once_with(["1234", "5678"])
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_sql_filter_contains_all_schluessel(
|
||||
self, mock_ask_text, mock_project, mock_wfs_layer_cls
|
||||
):
|
||||
"""Der WFS-URI enthält alle eingegebenen Gemarkungsschlüssel."""
|
||||
mock_ask_text.return_value = ("1111, 2222, 3333", True)
|
||||
mock_project.return_value = _DummyProject()
|
||||
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
|
||||
def test_sql_filter_contains_all_schluessel(self, mock_wfs_layer_cls):
|
||||
"""Prüft, dass lade_flurstuecke_per_gemarkung für jeden Gemarkungsschlüssel eine eigene WFS-Anfrage mit dessen Schlüssel stellt."""
|
||||
# Ungültig genügt: nur die aufgebaute URI je Aufruf wird geprüft, das
|
||||
# tatsächliche Zusammenführen der Features ist nicht Teil dieses Tests.
|
||||
mock_wfs_layer_cls.return_value = _DummyLayer(valid=False)
|
||||
|
||||
flurstueck_alkis(None)
|
||||
lade_flurstuecke_per_gemarkung(["1111", "2222", "3333"])
|
||||
|
||||
# Erster Positionsarg von QgsVectorLayer ist der URI-String
|
||||
call_args = mock_wfs_layer_cls.call_args
|
||||
uri_str = call_args[0][0] if call_args[0] else call_args[1].get("path", "")
|
||||
self.assertIn("1111", uri_str)
|
||||
self.assertIn("2222", uri_str)
|
||||
self.assertIn("3333", uri_str)
|
||||
uris = [call.args[0] for call in mock_wfs_layer_cls.call_args_list]
|
||||
self.assertEqual(len(uris), 3)
|
||||
self.assertIn("1111", uris[0])
|
||||
self.assertIn("2222", uris[1])
|
||||
self.assertIn("3333", uris[2])
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 4. Bestehender Layer – Nutzer wählt "Neu laden" #
|
||||
# 4. Bestehender Layer – Nutzer klickt "Überschreiben" #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_existing_layer_reload_yes(
|
||||
self, mock_ask_text, mock_project, mock_msgbox_cls, mock_wfs_layer_cls
|
||||
self, mock_ask_text, mock_project, mock_msgbox_cls, mock_lade
|
||||
):
|
||||
"""Bestehender Layer + Nutzer: 'Ja, neu laden' → LoadStatus.RELOAD."""
|
||||
"""Prüft, dass bei bestehendem Layer und Klick auf 'Überschreiben' der alte Layer entfernt und LoadStatus.RELOAD geliefert wird."""
|
||||
existing = _DummyLayer(valid=True, layer_id="old_flst")
|
||||
project = _DummyProject(named_layers=[existing])
|
||||
mock_ask_text.return_value = ("1234", True)
|
||||
mock_project.return_value = project
|
||||
|
||||
# StandardButton als int, damit | Operator funktioniert
|
||||
mock_msgbox_cls.StandardButton.Yes = 0x4000
|
||||
mock_msgbox_cls.StandardButton.No = 0x10000
|
||||
# QMessageBox.question → Yes
|
||||
mock_msgbox_cls.question.return_value = 0x4000
|
||||
# Drei-Knopf-Dialog: addButton() liefert je Aufruf einen eigenen
|
||||
# Sentinel, clickedButton() simuliert den Klick auf "Überschreiben".
|
||||
btn_ueberschreiben, btn_anhaengen, btn_abbrechen = object(), object(), object()
|
||||
msg_instance = mock_msgbox_cls.return_value
|
||||
msg_instance.addButton.side_effect = [btn_ueberschreiben, btn_anhaengen, btn_abbrechen]
|
||||
msg_instance.clickedButton.return_value = btn_ueberschreiben
|
||||
|
||||
new_layer = _DummyLayer(valid=True, layer_id="new_flst")
|
||||
mock_wfs_layer_cls.return_value = new_layer
|
||||
mock_lade.return_value = new_layer
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
@@ -192,46 +197,46 @@ class TestFlurstueckAlkis(unittest.TestCase):
|
||||
self.assertEqual(status, LoadStatus.RELOAD)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 5. Bestehender Layer – Nutzer wählt "Behalten" #
|
||||
# 5. Bestehender Layer – Nutzer klickt "Abbrechen" #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QMessageBox")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_existing_layer_keep_no(
|
||||
def test_existing_layer_abbrechen_returns_none(
|
||||
self, mock_ask_text, mock_project, mock_msgbox_cls
|
||||
):
|
||||
"""Bestehender Layer + Nutzer: 'Nein, behalten' → LoadStatus.KEEP."""
|
||||
"""Prüft, dass bei bestehendem Layer und Klick auf 'Abbrechen' nichts verändert wird und (None, LoadStatus.NONE) geliefert wird."""
|
||||
existing = _DummyLayer(valid=True, layer_id="keep_flst")
|
||||
project = _DummyProject(named_layers=[existing])
|
||||
mock_ask_text.return_value = ("1234", True)
|
||||
mock_project.return_value = project
|
||||
|
||||
# StandardButton als int, damit | Operator funktioniert
|
||||
mock_msgbox_cls.StandardButton.Yes = 0x4000
|
||||
mock_msgbox_cls.StandardButton.No = 0x10000
|
||||
# QMessageBox.question → No
|
||||
mock_msgbox_cls.question.return_value = 0x10000
|
||||
btn_ueberschreiben, btn_anhaengen, btn_abbrechen = object(), object(), object()
|
||||
msg_instance = mock_msgbox_cls.return_value
|
||||
msg_instance.addButton.side_effect = [btn_ueberschreiben, btn_anhaengen, btn_abbrechen]
|
||||
msg_instance.clickedButton.return_value = btn_abbrechen
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
self.assertIs(layer, existing)
|
||||
self.assertEqual(status, LoadStatus.KEEP)
|
||||
self.assertIsNone(layer)
|
||||
self.assertEqual(status, LoadStatus.NONE)
|
||||
self.assertEqual(project.removed, []) # nichts gelöscht
|
||||
mock_ask_text.assert_not_called() # Abbruch schon vor der Schlüsselabfrage
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 6. vorgabe_schluessel füllt ask_text-Default vor #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_vorgabe_schluessel_prefills_default_text(
|
||||
self, mock_ask_text, mock_project, mock_wfs_layer_cls
|
||||
self, mock_ask_text, mock_project, mock_lade
|
||||
):
|
||||
"""vorgabe_schluessel=['1234','5678'] → ask_text erhält '1234, 5678' als default_text."""
|
||||
"""Prüft, dass vorgabe_schluessel als vorbelegter default_text im Eingabedialog erscheint."""
|
||||
mock_ask_text.return_value = ("1234, 5678", True)
|
||||
mock_project.return_value = _DummyProject()
|
||||
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
|
||||
mock_lade.return_value = _DummyLayer(valid=True)
|
||||
|
||||
flurstueck_alkis(None, vorgabe_schluessel=["1234", "5678"])
|
||||
|
||||
@@ -245,16 +250,16 @@ class TestFlurstueckAlkis(unittest.TestCase):
|
||||
# ------------------------------------------------------------------ #
|
||||
# 7. vorgabe_namen erscheint im Label-Text #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_vorgabe_namen_appears_in_label(
|
||||
self, mock_ask_text, mock_project, mock_wfs_layer_cls
|
||||
self, mock_ask_text, mock_project, mock_lade
|
||||
):
|
||||
"""vorgabe_namen=['Musterstadt'] → ask_text-label enthält den Namen."""
|
||||
"""Prüft, dass vorgabe_namen im Beschriftungstext des Eingabedialogs erscheint."""
|
||||
mock_ask_text.return_value = ("9999", True)
|
||||
mock_project.return_value = _DummyProject()
|
||||
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
|
||||
mock_lade.return_value = _DummyLayer(valid=True)
|
||||
|
||||
flurstueck_alkis(None, vorgabe_namen=["Musterstadt"])
|
||||
|
||||
@@ -264,49 +269,179 @@ class TestFlurstueckAlkis(unittest.TestCase):
|
||||
self.assertIn("Musterstadt", label_arg)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 8. bbox wird in WFS-URI eingebettet #
|
||||
# 8. Rückwärtskompatibilität: alle neuen Params optional #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_bbox_embedded_in_wfs_uri(
|
||||
self, mock_ask_text, mock_project, mock_wfs_layer_cls
|
||||
):
|
||||
"""bbox-Parameter → WFS-URI enthält 'bbox=' Substring."""
|
||||
mock_ask_text.return_value = ("1234", True)
|
||||
mock_project.return_value = _DummyProject()
|
||||
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
|
||||
|
||||
class _Rect:
|
||||
def xMinimum(self): return 414000.0
|
||||
def yMinimum(self): return 5657000.0
|
||||
def xMaximum(self): return 415000.0
|
||||
def yMaximum(self): return 5658000.0
|
||||
|
||||
flurstueck_alkis(None, bbox=_Rect())
|
||||
|
||||
uri_str = mock_wfs_layer_cls.call_args[0][0]
|
||||
self.assertIn("bbox=", uri_str.lower())
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 9. Rückwärtskompatibilität: alle neuen Params optional #
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsVectorLayer")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_backwards_compatible_no_new_params(
|
||||
self, mock_ask_text, mock_project, mock_wfs_layer_cls
|
||||
self, mock_ask_text, mock_project, mock_lade
|
||||
):
|
||||
"""flurstueck_alkis(tab) ohne neue Parameter → kein Fehler."""
|
||||
"""Prüft, dass flurstueck_alkis() ohne die neuen optionalen Parameter weiterhin fehlerfrei einen Layer mit LoadStatus.FIRST liefert."""
|
||||
mock_ask_text.return_value = ("0000", True)
|
||||
mock_project.return_value = _DummyProject()
|
||||
mock_wfs_layer_cls.return_value = _DummyLayer(valid=True)
|
||||
mock_lade.return_value = _DummyLayer(valid=True)
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
self.assertIsNotNone(layer)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 9. ohne_schluessel_abfrage: vorgabe_schluessel direkt, kein ask_text (#47)
|
||||
# ------------------------------------------------------------------ #
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_ohne_schluessel_abfrage_ueberspringt_ask_text(
|
||||
self, mock_ask_text, mock_project, mock_lade
|
||||
):
|
||||
"""Prüft, dass ohne_schluessel_abfrage=True vorgabe_schluessel direkt verwendet und ask_text nicht aufgerufen wird."""
|
||||
mock_project.return_value = _DummyProject()
|
||||
geladen = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")])
|
||||
mock_lade.return_value = geladen
|
||||
|
||||
layer, status = flurstueck_alkis(
|
||||
None, vorgabe_schluessel=["1234", "5678"], ohne_schluessel_abfrage=True
|
||||
)
|
||||
|
||||
mock_ask_text.assert_not_called()
|
||||
self.assertIs(layer, geladen)
|
||||
self.assertEqual(status, LoadStatus.FIRST)
|
||||
mock_lade.assert_called_once_with(["1234", "5678"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dummies für den Anhängen-Deduplizierungstest (#28)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _DummyFlstFeature:
|
||||
def __init__(self, kennzeichen):
|
||||
self._kennzeichen = kennzeichen
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == FELD_FLURSTUECKSKENNZEICHEN:
|
||||
return self._kennzeichen
|
||||
return None
|
||||
|
||||
|
||||
class _DummyFlstFields:
|
||||
def __init__(self, hat_kennzeichen_feld: bool):
|
||||
self._hat_feld = hat_kennzeichen_feld
|
||||
|
||||
def indexFromName(self, name):
|
||||
if name == FELD_FLURSTUECKSKENNZEICHEN and self._hat_feld:
|
||||
return 0
|
||||
return -1
|
||||
|
||||
|
||||
class _DummyFlstLayer:
|
||||
"""Flurstück-Layer-Dummy mit Feature-Liste und aufzeichnendem ``dataProvider``."""
|
||||
|
||||
def __init__(self, features, hat_kennzeichen_feld: bool = True):
|
||||
self._features = list(features)
|
||||
self._hat_feld = hat_kennzeichen_feld
|
||||
self.added = None
|
||||
|
||||
def getFeatures(self):
|
||||
return list(self._features)
|
||||
|
||||
def fields(self):
|
||||
return _DummyFlstFields(self._hat_feld)
|
||||
|
||||
def dataProvider(self):
|
||||
layer = self
|
||||
|
||||
class _Provider:
|
||||
def addFeatures(self, feats):
|
||||
layer.added = list(feats)
|
||||
layer._features.extend(feats)
|
||||
return True
|
||||
|
||||
return _Provider()
|
||||
|
||||
def updateExtents(self):
|
||||
pass
|
||||
|
||||
def triggerRepaint(self):
|
||||
pass
|
||||
|
||||
def isValid(self):
|
||||
return True
|
||||
|
||||
def featureCount(self):
|
||||
return len(self._features)
|
||||
|
||||
|
||||
class TestFlurstueckAlkisAnhaengenDedup(unittest.TestCase):
|
||||
"""Anhängen an einen bestehenden Flurstück-Layer (Issue #28)."""
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis._check_existing_flurstueck_layer")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_append_ueberspringt_bereits_vorhandene_flurstuecke(
|
||||
self, mock_ask_text, mock_check, mock_lade
|
||||
):
|
||||
"""Prüft, dass beim Anhängen bereits vorhandene flurstueckskennzeichen nicht erneut hinzugefügt werden."""
|
||||
mock_ask_text.return_value = ("0001, 0002", True)
|
||||
bestehend = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")])
|
||||
neu_geladen = _DummyFlstLayer([
|
||||
_DummyFlstFeature("110001_0001__00001"), # bereits im Layer vorhanden
|
||||
_DummyFlstFeature("110001_0002__00007"), # noch nicht vorhanden
|
||||
])
|
||||
mock_check.return_value = (bestehend, LoadStatus.APPEND)
|
||||
mock_lade.return_value = neu_geladen
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
self.assertEqual(status, LoadStatus.APPEND)
|
||||
self.assertIs(layer, bestehend)
|
||||
self.assertEqual(len(bestehend.added), 1)
|
||||
self.assertEqual(bestehend.added[0][FELD_FLURSTUECKSKENNZEICHEN], "110001_0002__00007")
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis._check_existing_flurstueck_layer")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_append_ohne_kennzeichen_feld_haengt_alles_an(
|
||||
self, mock_ask_text, mock_check, mock_lade
|
||||
):
|
||||
"""Prüft, dass ohne flurstueckskennzeichen-Feld weiterhin alle Features angehängt werden (Rückfallverhalten)."""
|
||||
mock_ask_text.return_value = ("0001", True)
|
||||
bestehend = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")], hat_kennzeichen_feld=False)
|
||||
neu_geladen = _DummyFlstLayer(
|
||||
[_DummyFlstFeature("110001_0001__00001"), _DummyFlstFeature("110001_0002__00007")],
|
||||
hat_kennzeichen_feld=False,
|
||||
)
|
||||
mock_check.return_value = (bestehend, LoadStatus.APPEND)
|
||||
mock_lade.return_value = neu_geladen
|
||||
|
||||
layer, status = flurstueck_alkis(None)
|
||||
|
||||
self.assertEqual(status, LoadStatus.APPEND)
|
||||
self.assertEqual(len(bestehend.added), 2)
|
||||
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.lade_flurstuecke_per_gemarkung")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis._check_existing_flurstueck_layer")
|
||||
@patch("sn_verfahrensgebiet.functions.flurstueck_alkis.ask_text")
|
||||
def test_ohne_schluessel_abfrage_append_dedup(self, mock_ask_text, mock_check, mock_lade):
|
||||
"""ohne_schluessel_abfrage=True: Anhängen dedupliziert trotzdem, ohne nach Schlüsseln zu fragen (#47)."""
|
||||
bestehend = _DummyFlstLayer([_DummyFlstFeature("110001_0001__00001")])
|
||||
neu_geladen = _DummyFlstLayer([
|
||||
_DummyFlstFeature("110001_0001__00001"), # bereits vorhanden
|
||||
_DummyFlstFeature("110001_0002__00007"), # neu
|
||||
])
|
||||
mock_check.return_value = (bestehend, LoadStatus.APPEND)
|
||||
mock_lade.return_value = neu_geladen
|
||||
|
||||
layer, status = flurstueck_alkis(
|
||||
None, vorgabe_schluessel=["0001", "0002"], ohne_schluessel_abfrage=True
|
||||
)
|
||||
|
||||
mock_ask_text.assert_not_called()
|
||||
self.assertEqual(status, LoadStatus.APPEND)
|
||||
self.assertEqual(len(bestehend.added), 1)
|
||||
self.assertEqual(bestehend.added[0][FELD_FLURSTUECKSKENNZEICHEN], "110001_0002__00007")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"""Tests für die Flurstückssuche im Layer „Flurstücke“ (#56)."""
|
||||
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from sn_verfahrensgebiet.functions import flurstueck_suche as fs
|
||||
|
||||
|
||||
class _Feature(dict):
|
||||
def __init__(self, fid, **attribute):
|
||||
super().__init__(attribute)
|
||||
self._fid = fid
|
||||
|
||||
def id(self):
|
||||
return self._fid
|
||||
|
||||
|
||||
class _Layer:
|
||||
def __init__(self, name, features, felder=("flurstueckskennzeichen",)):
|
||||
self._name = name
|
||||
self._features = features
|
||||
self._felder = list(felder)
|
||||
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def fields(self):
|
||||
return types.SimpleNamespace(
|
||||
indexFromName=lambda n: self._felder.index(n) if n in self._felder else -1
|
||||
)
|
||||
|
||||
def getFeatures(self):
|
||||
return iter(self._features)
|
||||
|
||||
|
||||
def _flurstuecke():
|
||||
return _Layer("Flurstücke", [
|
||||
_Feature(1, flurstueckskennzeichen="140268___00562____00"),
|
||||
_Feature(2, flurstueckskennzeichen="140268___00562000100"),
|
||||
_Feature(3, flurstueckskennzeichen="140268___00012000300"),
|
||||
_Feature(4, flurstueckskennzeichen="140269___00562____00"),
|
||||
_Feature(5, flurstueckskennzeichen="140268___00012000a00"),
|
||||
_Feature(6, flurstueckskennzeichen=None),
|
||||
])
|
||||
|
||||
|
||||
def _gemarkungen():
|
||||
return _Layer("Gemarkungen", [
|
||||
_Feature(1, schluessel="140640268", ave_name="Beispielhausen"),
|
||||
_Feature(2, schluessel="140640269", ave_name="Musterdorf"),
|
||||
_Feature(3, schluessel="140650270", ave_name="Musterdorf"),
|
||||
], felder=("schluessel", "ave_name"))
|
||||
|
||||
|
||||
def _projekt(*layer):
|
||||
nach_name = {lyr.name(): lyr for lyr in layer}
|
||||
projekt = types.SimpleNamespace(
|
||||
mapLayersByName=lambda name: [nach_name[name]] if name in nach_name else []
|
||||
)
|
||||
return types.SimpleNamespace(instance=lambda: projekt)
|
||||
|
||||
|
||||
class TestEingabe(unittest.TestCase):
|
||||
|
||||
def test_flurstuecksnummer_mit_und_ohne_nenner(self):
|
||||
"""Prüft, dass Zähler und Nenner getrennt und führende Nullen entfernt werden."""
|
||||
self.assertEqual(fs.zerlege_flurstuecksnummer("562"), ("562", ""))
|
||||
self.assertEqual(fs.zerlege_flurstuecksnummer(" 0012 / 03 "), ("12", "3"))
|
||||
self.assertEqual(fs.zerlege_flurstuecksnummer("12/A"), ("12", "a"))
|
||||
|
||||
def test_ungueltige_flurstuecksnummer(self):
|
||||
"""Prüft, dass leere oder unzulässige Flurstücksnummern abgelehnt werden."""
|
||||
for text in ("", "abc", "12/", "12/3/4", "12-3"):
|
||||
with self.subTest(text=text):
|
||||
with self.assertRaises(fs.FlurstueckSucheFehler):
|
||||
fs.zerlege_flurstuecksnummer(text)
|
||||
|
||||
def test_kennzeichen_zerlegen(self):
|
||||
"""Prüft, dass Gemarkung, Zähler und Nenner aus dem ALKIS-Kennzeichen gelesen werden."""
|
||||
self.assertEqual(fs.zerlege_kennzeichen("140268___00562____00"), ("0268", "562", ""))
|
||||
self.assertEqual(fs.zerlege_kennzeichen("140268___00012000300"), ("0268", "12", "3"))
|
||||
self.assertEqual(fs.zerlege_kennzeichen("140268___00012000a00"), ("0268", "12", "a"))
|
||||
self.assertIsNone(fs.zerlege_kennzeichen("1402"))
|
||||
self.assertIsNone(fs.zerlege_kennzeichen(None))
|
||||
|
||||
def test_gemarkungsnummer_aus_ziffern(self):
|
||||
"""Prüft, dass Ziffern als Gemarkungsnummer gelten, auch ohne führende Nullen oder als langer Schlüssel."""
|
||||
self.assertEqual(fs.gemarkungsnummern("268"), ["0268"])
|
||||
self.assertEqual(fs.gemarkungsnummern("140268"), ["0268"])
|
||||
|
||||
def test_gemarkungsname_ueber_gemarkungslayer(self):
|
||||
"""Prüft, dass ein Gemarkungsname ohne Rücksicht auf Großschreibung in alle passenden Nummern übersetzt wird."""
|
||||
self.assertEqual(fs.gemarkungsnummern("beispielhausen", _gemarkungen()), ["0268"])
|
||||
self.assertEqual(fs.gemarkungsnummern("Musterdorf", _gemarkungen()), ["0269", "0270"])
|
||||
|
||||
def test_gemarkungsname_ohne_layer_oder_unbekannt(self):
|
||||
"""Prüft, dass ein Name ohne Gemarkungslayer oder ohne Treffer eine verständliche Meldung ergibt."""
|
||||
with self.assertRaisesRegex(fs.FlurstueckSucheFehler, "Gemarkungsnummer"):
|
||||
fs.gemarkungsnummern("Beispielhausen", None)
|
||||
with self.assertRaisesRegex(fs.FlurstueckSucheFehler, "keine Gemarkung"):
|
||||
fs.gemarkungsnummern("Nirgendwo", _gemarkungen())
|
||||
with self.assertRaises(fs.FlurstueckSucheFehler):
|
||||
fs.gemarkungsnummern(" ")
|
||||
|
||||
|
||||
class TestSuche(unittest.TestCase):
|
||||
|
||||
def test_mit_nenner_genau_ein_treffer(self):
|
||||
"""Prüft, dass mit Nenner nur das passende Flurstück der Gemarkung gefunden wird."""
|
||||
self.assertEqual(fs.finde_flurstuecke(_flurstuecke(), ["0268"], "12", "3"), [3])
|
||||
self.assertEqual(fs.finde_flurstuecke(_flurstuecke(), ["0268"], "12", "a"), [5])
|
||||
|
||||
def test_ohne_nenner_alle_mit_diesem_zaehler(self):
|
||||
"""Prüft, dass ohne Nenner alle Flurstücke mit dem Zähler gefunden werden, aber nur in der gesuchten Gemarkung."""
|
||||
self.assertEqual(fs.finde_flurstuecke(_flurstuecke(), ["0268"], "562", ""), [1, 2])
|
||||
self.assertEqual(fs.finde_flurstuecke(_flurstuecke(), ["0268", "0269"], "562", ""), [1, 2, 4])
|
||||
|
||||
def test_kein_treffer(self):
|
||||
"""Prüft, dass ein nicht vorhandenes Flurstück eine leere Trefferliste ergibt."""
|
||||
self.assertEqual(fs.finde_flurstuecke(_flurstuecke(), ["0268"], "999", ""), [])
|
||||
|
||||
def test_layer_ohne_kennzeichenfeld(self):
|
||||
"""Prüft, dass ein Flurstückslayer ohne Feld „flurstueckskennzeichen“ gemeldet wird."""
|
||||
layer = _Layer("Flurstücke", [], felder=("gml_id",))
|
||||
with self.assertRaisesRegex(fs.FlurstueckSucheFehler, "flurstueckskennzeichen"):
|
||||
fs.finde_flurstuecke(layer, ["0268"], "562", "")
|
||||
|
||||
def test_fehlender_flurstueckslayer_bricht_ab(self):
|
||||
"""Prüft, dass die Suche ohne Layer „Flurstücke“ mit dem Hinweis abbricht, zuerst Verfahrensgebiet und Flurstücke zu laden."""
|
||||
with patch.object(fs, "QgsProject", _projekt(_gemarkungen())):
|
||||
with self.assertRaises(fs.KeinFlurstueckslayer) as kontext:
|
||||
fs.flurstueck_suchen("0268", "562")
|
||||
self.assertIn("Flurstücke aus ALKIS laden", str(kontext.exception))
|
||||
|
||||
def test_suche_im_projekt_mit_gemarkungsname(self):
|
||||
"""Prüft, dass die Suche im Projekt den Gemarkungsnamen auflöst und Layer samt Treffern liefert."""
|
||||
flurstuecke = _flurstuecke()
|
||||
with patch.object(fs, "QgsProject", _projekt(flurstuecke, _gemarkungen())):
|
||||
ergebnis = fs.flurstueck_suchen("Beispielhausen", "12/3")
|
||||
self.assertIs(ergebnis.layer, flurstuecke)
|
||||
self.assertEqual(ergebnis.feature_ids, [3])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -43,6 +43,7 @@ if "qgis" not in sys.modules:
|
||||
|
||||
from sn_verfahrensgebiet.functions.grenzpunkt_alkis import (
|
||||
grenzpunkt_alkis,
|
||||
grenzpunkte_gemarkungsweise,
|
||||
_extrahiere_gml_ids,
|
||||
)
|
||||
|
||||
@@ -297,5 +298,134 @@ class TestGrenzpunktAlkis(unittest.TestCase):
|
||||
self.assertIs(gp, gp_layer)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: grenzpunkte_gemarkungsweise – Stilanbringung (#53)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_G_PATCH_EXTENTS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._lade_gemarkung_extents"
|
||||
_G_PATCH_HITS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._wfs_hits_count"
|
||||
_G_PATCH_GET_GML = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._wfs_get_gml"
|
||||
_G_PATCH_PARSE_GML = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._parse_alkis_gml_memory_layer"
|
||||
_G_PATCH_ERKENNE_FELD = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._erkenne_gehoert_zu_feld"
|
||||
_G_PATCH_EXTRAHIERE_ID = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._extrahiere_feature_gml_id"
|
||||
_G_PATCH_LADE_GP_DICT = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._lade_grenzpunkte_als_dict"
|
||||
_G_PATCH_ERSTELLE_JOINED = "sn_verfahrensgebiet.functions.grenzpunkt_alkis._erstelle_joined_layer"
|
||||
_G_PATCH_WFS_LAYER = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsVectorLayer"
|
||||
_G_PATCH_PROJECT = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.QgsProject"
|
||||
_G_PATCH_PROGRESS = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.create_progress_dialog"
|
||||
_G_PATCH_APPLY_STIL = "sn_verfahrensgebiet.functions.grenzpunkt_alkis.apply_style"
|
||||
# QgsField/QgsFeature werden innerhalb der Funktion per lokalem Import geholt
|
||||
# (from sn_basis.functions import QgsField/QgsFeature) - Patch-Ziel ist daher
|
||||
# die Quelle, nicht der Aufrufort.
|
||||
_G_PATCH_QGSFIELD = "sn_basis.functions.QgsField"
|
||||
_G_PATCH_QGSFEATURE = "sn_basis.functions.QgsFeature"
|
||||
|
||||
|
||||
class _DummyProgress:
|
||||
def set_value(self, wert): pass
|
||||
def set_label(self, text): pass
|
||||
def set_total(self, wert): pass
|
||||
def is_canceled(self): return False
|
||||
def close(self): pass
|
||||
|
||||
|
||||
class _DummyBbox:
|
||||
def xMinimum(self): return 414000.0
|
||||
def yMinimum(self): return 5657000.0
|
||||
def xMaximum(self): return 415000.0
|
||||
def yMaximum(self): return 5658000.0
|
||||
|
||||
|
||||
class _DummyGmlFeld:
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
|
||||
class _DummyGmlFeature:
|
||||
def __init__(self, gehoert_zu_wert):
|
||||
self._wert = gehoert_zu_wert
|
||||
|
||||
def geometry(self):
|
||||
return object()
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == "gehoertZu":
|
||||
return self._wert
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
class _DummyGmlLayer:
|
||||
"""Minimaler Ersatz für das Ergebnis von _parse_alkis_gml_memory_layer()."""
|
||||
|
||||
def __init__(self, feature):
|
||||
self._feature = feature
|
||||
|
||||
def fields(self):
|
||||
return [_DummyGmlFeld("gehoertZu")]
|
||||
|
||||
def getFeatures(self):
|
||||
return [self._feature]
|
||||
|
||||
|
||||
class TestGrenzpunkteGemarkungsweiseStil(unittest.TestCase):
|
||||
|
||||
@patch(_G_PATCH_APPLY_STIL)
|
||||
@patch(_G_PATCH_QGSFEATURE)
|
||||
@patch(_G_PATCH_QGSFIELD)
|
||||
@patch(_G_PATCH_PROGRESS)
|
||||
@patch(_G_PATCH_PROJECT)
|
||||
@patch(_G_PATCH_WFS_LAYER)
|
||||
@patch(_G_PATCH_ERSTELLE_JOINED)
|
||||
@patch(_G_PATCH_LADE_GP_DICT)
|
||||
@patch(_G_PATCH_EXTRAHIERE_ID)
|
||||
@patch(_G_PATCH_ERKENNE_FELD)
|
||||
@patch(_G_PATCH_PARSE_GML)
|
||||
@patch(_G_PATCH_GET_GML)
|
||||
@patch(_G_PATCH_HITS)
|
||||
@patch(_G_PATCH_EXTENTS)
|
||||
def test_stil_wird_nach_dem_laden_auf_den_ergebnis_layer_angewendet(
|
||||
self,
|
||||
mock_extents,
|
||||
mock_hits,
|
||||
mock_get_gml,
|
||||
mock_parse_gml,
|
||||
mock_erkenne_feld,
|
||||
mock_extrahiere_id,
|
||||
mock_lade_gp_dict,
|
||||
mock_erstelle_joined,
|
||||
mock_wfs_layer_cls,
|
||||
mock_project_cls,
|
||||
mock_progress,
|
||||
mock_qgsfield,
|
||||
mock_qgsfeature,
|
||||
mock_apply_style,
|
||||
):
|
||||
"""Prüft, dass grenzpunkte_gemarkungsweise() nach dem Laden apply_style() mit dem neuen ALKIS-Grenzpunkt-Stil auf den fertigen Join-Layer anwendet (#53)."""
|
||||
mock_extents.return_value = {"1234": _DummyBbox()}
|
||||
mock_hits.return_value = 1
|
||||
mock_get_gml.return_value = b"<fake/>"
|
||||
mock_parse_gml.return_value = _DummyGmlLayer(_DummyGmlFeature("urn:adv:oid:GP1"))
|
||||
mock_erkenne_feld.return_value = "gehoertZu"
|
||||
mock_extrahiere_id.return_value = "PTA1"
|
||||
mock_lade_gp_dict.return_value = ({"GP1": object()}, ["gp_feld"])
|
||||
|
||||
joined_layer = MagicMock()
|
||||
mock_erstelle_joined.return_value = joined_layer
|
||||
mock_wfs_layer_cls.return_value = MagicMock()
|
||||
|
||||
project = MagicMock()
|
||||
mock_project_cls.instance.return_value = project
|
||||
mock_progress.return_value = _DummyProgress()
|
||||
|
||||
ergebnis = grenzpunkte_gemarkungsweise(None, ["1234"])
|
||||
|
||||
self.assertIs(ergebnis, joined_layer)
|
||||
project.addMapLayer.assert_called_once_with(joined_layer)
|
||||
mock_apply_style.assert_called_once_with(joined_layer, "alkis_grenzpunkte.qml")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Tests für die gemeinsame Auswahltabelle in verfahrensgebiet_dialoge (#56)."""
|
||||
|
||||
import types
|
||||
import unittest
|
||||
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge import (
|
||||
ausgewaehlte_indizes,
|
||||
zeilen_sichtbar,
|
||||
)
|
||||
|
||||
|
||||
class _Item:
|
||||
def __init__(self, row, index):
|
||||
self._row = row
|
||||
self._index = index
|
||||
|
||||
def row(self):
|
||||
return self._row
|
||||
|
||||
def data(self, _rolle):
|
||||
return self._index
|
||||
|
||||
|
||||
class TestAuswahltabelle(unittest.TestCase):
|
||||
|
||||
def test_suchfilter_ueber_alle_spalten(self):
|
||||
"""Prüft, dass der Suchfilter Teilstrings in jeder Spalte ohne Groß-/Kleinschreibung findet und leer alles zeigt."""
|
||||
zeilen = [["14612", "Verfahren Nord"], ["14613", "Süd"], ["14700", None]]
|
||||
self.assertEqual(zeilen_sichtbar(zeilen, "nord"), [True, False, False])
|
||||
self.assertEqual(zeilen_sichtbar(zeilen, " 146 "), [True, True, False])
|
||||
self.assertEqual(zeilen_sichtbar(zeilen, ""), [True, True, True])
|
||||
|
||||
def test_auswahl_nach_umsortieren(self):
|
||||
"""Prüft, dass die Auswahl über den gespeicherten Index auf die ursprünglichen Zeilen zurückgeführt wird, auch wenn die Tabelle umsortiert ist."""
|
||||
# Tabelle zeigt Zeile 0 → Index 2, Zeile 1 → Index 0; beide Spalten je Zeile ausgewählt
|
||||
tabelle = types.SimpleNamespace(
|
||||
selectedItems=lambda: [_Item(0, 2), _Item(0, 2), _Item(1, 0), _Item(1, 0)],
|
||||
item=lambda row, col: _Item(row, {0: 2, 1: 0}[row]),
|
||||
)
|
||||
self.assertEqual(ausgewaehlte_indizes(tabelle), [0, 2])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -81,6 +81,30 @@ class _DummyCombo:
|
||||
self.layer = layer
|
||||
|
||||
|
||||
class _DummyGemFeature:
|
||||
def __init__(self, schluessel, name):
|
||||
self._schluessel = schluessel
|
||||
self._name = name
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key == "schluessel":
|
||||
return self._schluessel
|
||||
if key == "ave_name":
|
||||
return self._name
|
||||
return None
|
||||
|
||||
|
||||
class _DummyGemarkungenLayer:
|
||||
def __init__(self, selected):
|
||||
self._selected = list(selected)
|
||||
|
||||
def selectedFeatureCount(self):
|
||||
return len(self._selected)
|
||||
|
||||
def selectedFeatures(self):
|
||||
return list(self._selected)
|
||||
|
||||
|
||||
class TestWorkingTab(unittest.TestCase):
|
||||
|
||||
def _make_tab(self):
|
||||
@@ -96,6 +120,7 @@ class TestWorkingTab(unittest.TestCase):
|
||||
tab.haken_flurst = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None)
|
||||
tab.haken_gem = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None)
|
||||
tab.haken_grenz = types.SimpleNamespace(setText=lambda _t: None, setStyleSheet=lambda _s: None)
|
||||
tab.btn_flurst_aus_gemarkungen = _DummyButton()
|
||||
tab.setze_haken = lambda *_args, **_kwargs: None
|
||||
# Pruefmanager-Mock: verarbeite() wird im Test ggf. überschrieben
|
||||
tab.pruefmanager = types.SimpleNamespace(
|
||||
@@ -175,6 +200,83 @@ class TestWorkingTab(unittest.TestCase):
|
||||
|
||||
mock_set_layer_id.assert_called_once_with("")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# handle_flurst_alkis / handle_flurst_aus_gemarkungen (#47: eine gemeinsame
|
||||
# Ladelogik in flurstueck_alkis(), hier wird nur noch die Anbindung geprüft)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
def test_handle_flurst_alkis_first_load_uebernimmt_layer(
|
||||
self, mock_iface, mock_project_instance, mock_flurstueck_alkis, mock_apply_style, mock_success
|
||||
):
|
||||
"""Erstmaliges Laden über 'Flurstücke laden' fügt den Layer dem Projekt hinzu."""
|
||||
tab = self._make_tab()
|
||||
mock_iface.mapCanvas.return_value = _DummyCanvas()
|
||||
project = _DummyProject()
|
||||
mock_project_instance.return_value = project
|
||||
layer = _DummyLayer(valid=True, layer_id="flst_first")
|
||||
mock_flurstueck_alkis.return_value = (layer, LoadStatus.FIRST)
|
||||
|
||||
tab.handle_flurst_alkis()
|
||||
|
||||
mock_flurstueck_alkis.assert_called_once_with(tab)
|
||||
self.assertIn(layer, project.added)
|
||||
mock_success.assert_called_once()
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
def test_handle_flurst_aus_gemarkungen_ruft_flurstueck_alkis_ohne_schluessel_abfrage(
|
||||
self, mock_iface, mock_project_instance, mock_flurstueck_alkis, mock_success
|
||||
):
|
||||
"""Der Gemarkungen-Auswahl-Button ruft dieselbe Ladefunktion mit ohne_schluessel_abfrage=True auf (#47)."""
|
||||
tab = self._make_tab()
|
||||
mock_iface.mapCanvas.return_value = _DummyCanvas()
|
||||
gem_feat = _DummyGemFeature("110001", "Musterstadt")
|
||||
gem_layer = _DummyGemarkungenLayer([gem_feat])
|
||||
project = types.SimpleNamespace(
|
||||
mapLayersByName=lambda name: [gem_layer] if name == "Gemarkungen" else []
|
||||
)
|
||||
mock_project_instance.return_value = project
|
||||
layer = _DummyLayer(valid=True, layer_id="flst_append")
|
||||
mock_flurstueck_alkis.return_value = (layer, LoadStatus.APPEND)
|
||||
|
||||
tab.handle_flurst_aus_gemarkungen()
|
||||
|
||||
mock_flurstueck_alkis.assert_called_once()
|
||||
_args, kwargs = mock_flurstueck_alkis.call_args
|
||||
self.assertEqual(kwargs.get("vorgabe_schluessel"), ["0001"])
|
||||
self.assertEqual(kwargs.get("vorgabe_namen"), ["Musterstadt"])
|
||||
self.assertTrue(kwargs.get("ohne_schluessel_abfrage"))
|
||||
mock_success.assert_called_once()
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.error")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.flurstueck_alkis")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
def test_handle_flurst_aus_gemarkungen_abbruch_zeigt_keine_zusaetzliche_meldung(
|
||||
self, mock_project_instance, mock_flurstueck_alkis, mock_success, mock_error
|
||||
):
|
||||
"""Bricht flurstueck_alkis() ab (None, NONE), zeigt der Button keine eigene Meldung – flurstueck_alkis() hat das schon getan (#47)."""
|
||||
tab = self._make_tab()
|
||||
gem_feat = _DummyGemFeature("110001", "Musterstadt")
|
||||
gem_layer = _DummyGemarkungenLayer([gem_feat])
|
||||
project = types.SimpleNamespace(
|
||||
mapLayersByName=lambda name: [gem_layer] if name == "Gemarkungen" else []
|
||||
)
|
||||
mock_project_instance.return_value = project
|
||||
mock_flurstueck_alkis.return_value = (None, LoadStatus.NONE)
|
||||
|
||||
tab.handle_flurst_aus_gemarkungen()
|
||||
|
||||
mock_success.assert_not_called()
|
||||
mock_error.assert_not_called()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# handle_grenz_alkis
|
||||
# ------------------------------------------------------------------
|
||||
@@ -822,6 +924,68 @@ class TestHandleVerfShape(unittest.TestCase):
|
||||
self.assertIn(False, busy_states, "Button muss am Ende wieder freigegeben werden")
|
||||
|
||||
|
||||
class TestZoomImProjektKbs(unittest.TestCase):
|
||||
"""Zoom auf das neue Verfahrensgebiet rechnet in das KBS der Kartenansicht um (Basis #99)."""
|
||||
|
||||
def _tab(self):
|
||||
tab = TestWorkingTab._make_tab(TestWorkingTab())
|
||||
tab.setze_haken = lambda *_: None
|
||||
tab._on_verf_layer_changed = lambda *_: None
|
||||
return tab
|
||||
|
||||
def _projekt(self, mock_project_instance):
|
||||
proj = _DummyProject()
|
||||
proj.mapLayersByName = lambda _n: []
|
||||
proj.mapLayer = lambda _lid: None
|
||||
mock_project_instance.return_value = proj
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.zoom_auf_layer")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_alkis_komplett")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||
def test_alkis_zoom_ueber_zoom_auf_layer(self, _set_id, mock_iface, mock_alkis, mock_projekt,
|
||||
_stil, _erfolg, mock_zoom):
|
||||
"""Prüft, dass nach dem Laden aus ALKIS über zoom_auf_layer auf das Verfahrensgebiet gezoomt wird und nicht mit der unumgerechneten Layer-Ausdehnung."""
|
||||
tab = self._tab()
|
||||
enriched = _DummyLayer(valid=True, layer_id="vg")
|
||||
mock_alkis.return_value = (enriched, _DummyLayer(layer_id="flst"), None, LoadStatus.FIRST)
|
||||
self._projekt(mock_projekt)
|
||||
canvas = _DummyCanvas()
|
||||
mock_iface.mapCanvas.return_value = canvas
|
||||
|
||||
tab.handle_verf_alkis()
|
||||
|
||||
mock_zoom.assert_called_once_with(enriched, canvas)
|
||||
self.assertIsNone(canvas.extent)
|
||||
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.get_variable", return_value="VF-123")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.zoom_auf_layer")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.success")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.apply_style")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.QgsProject.instance")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.verfahrensgebiet_shape_komplett")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.iface")
|
||||
@patch("sn_verfahrensgebiet.ui.tabs.working_tab.set_verfahrensgebiet_layer_id")
|
||||
def test_shape_zoom_ueber_zoom_auf_layer(self, _set_id, mock_iface, mock_shape, mock_projekt,
|
||||
_stil, _erfolg, mock_zoom, _var):
|
||||
"""Prüft, dass nach dem Laden aus einem Shape über zoom_auf_layer auf das Verfahrensgebiet gezoomt wird und nicht mit der unumgerechneten Layer-Ausdehnung."""
|
||||
tab = self._tab()
|
||||
tab.btn_verf_shape = _DummyButton()
|
||||
enriched = _DummyLayer(valid=True, layer_id="shape_vg")
|
||||
mock_shape.return_value = (enriched, _DummyLayer(layer_id="flst"), None, LoadStatus.FIRST)
|
||||
self._projekt(mock_projekt)
|
||||
canvas = _DummyCanvas()
|
||||
mock_iface.mapCanvas.return_value = canvas
|
||||
|
||||
tab.handle_verf_shape()
|
||||
|
||||
mock_zoom.assert_called_once_with(enriched, canvas)
|
||||
self.assertIsNone(canvas.extent)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
+5
-4
@@ -1,10 +1,11 @@
|
||||
"""sn_verfahrensgebiet/ui/dockwidget.py – Haupt-DockWidget für sn_verfahrensgebiet.
|
||||
Verwaltet die Tabs für Verfahrensgebiet-Bearbeitung, Druck und Einstellungen.
|
||||
Verwaltet die Tabs für Verfahrensgebiet-Bearbeitung, Eigentümer, Druck und Einstellungen.
|
||||
"""
|
||||
|
||||
from sn_basis.ui.tabs.settings_tab import SettingsTab
|
||||
from sn_basis.ui.tabs.print_tab import PrintTab
|
||||
from sn_verfahrensgebiet.ui.tabs.working_tab import WorkingTab
|
||||
from sn_verfahrensgebiet.ui.tabs.eigentuemer_tab import EigentuemerTab
|
||||
from sn_basis.ui.base_dockwidget import BaseDockWidget
|
||||
from sn_basis.functions.qt_wrapper import QTabWidget
|
||||
from sn_basis.functions.message_wrapper import error
|
||||
@@ -14,12 +15,12 @@ class DockWidget(BaseDockWidget):
|
||||
"""Haupt-DockWidget für sn_verfahrensgebiet mit Tab-Verwaltung.
|
||||
|
||||
Attributes:
|
||||
tabs: Liste von Tab-Klassen (WorkingTab, PrintTab, SettingsTab)
|
||||
tabs: Liste von Tab-Klassen (WorkingTab, EigentuemerTab, PrintTab, SettingsTab)
|
||||
pruefmanager: Prüfmanager-Instanz für Validierung
|
||||
data_grabber: DataGrabber-Instanz für Fachdatenabruf
|
||||
"""
|
||||
|
||||
tabs = [WorkingTab, PrintTab, SettingsTab]
|
||||
tabs = [WorkingTab, EigentuemerTab, PrintTab, SettingsTab]
|
||||
|
||||
def __init__(self, parent=None, subtitle="", pruefmanager=None, data_grabber=None):
|
||||
"""Initialisiert das DockWidget mit Services und Tabs.
|
||||
@@ -38,7 +39,7 @@ class DockWidget(BaseDockWidget):
|
||||
self._init_tabs_with_services()
|
||||
|
||||
def _init_tabs_with_services(self):
|
||||
"""Tabs neu aufbauen und Services (z.B. fuer PrintTab) injizieren."""
|
||||
"""Tabs neu aufbauen und Services (z.B. für PrintTab) injizieren."""
|
||||
try:
|
||||
self.setWidget(None)
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"""sn_verfahrensgebiet/ui/eigentuemer_anzeige.py – Anzeigefenster für den Eigentümer (#56).
|
||||
|
||||
Das Fenster ist für einen zweiten Bildschirm gedacht, den nur der Eigentümer
|
||||
sieht. Es zeigt ausschließlich Name, Anschrift und E-Mail der vom Bearbeiter
|
||||
gewählten Einträge – keine Konten, keine Kennzeichen, keine anderen Treffer.
|
||||
Ohne Inhalt zeigt es nur einen neutralen Platzhalter.
|
||||
|
||||
Das Fenster öffnet immer als normales Fenster mit Rahmen; der Bearbeiter
|
||||
schiebt es selbst auf den Bildschirm des Eigentümers und kann es dort
|
||||
maximieren. Eine automatische Bildschirmwahl mit Vollbild hat sich im QGIS-Test
|
||||
nicht bewährt: Das Fenster landete stets auf dem Hauptbildschirm und ließ sich
|
||||
im Vollbild weder verschieben noch schließen (siehe ``fallstricke.md``).
|
||||
"""
|
||||
|
||||
import html
|
||||
from typing import Callable, Optional, Sequence
|
||||
|
||||
from sn_basis.functions.qt_wrapper import QWidget, QVBoxLayout, QLabel, Qt
|
||||
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer
|
||||
|
||||
PLATZHALTER = "Datenabgleich"
|
||||
OHNE_EMAIL = "keine angegeben"
|
||||
STANDARDGROESSE = (900, 650)
|
||||
|
||||
|
||||
def _ortsteil(text: str) -> str:
|
||||
return text if not text or text.upper().startswith("OT") else f"OT {text}"
|
||||
|
||||
|
||||
def anzeige_html(eintraege: Sequence[Eigentuemer]) -> str:
|
||||
"""HTML für das Anzeigefenster; alle Werte sind maskiert.
|
||||
|
||||
Je Eintrag eine Tabelle mit Name, Anschrift (mehrzeilig) und E-Mail. Eine
|
||||
fehlende E-Mail wird ausdrücklich genannt, damit der Eigentümer sie ergänzen
|
||||
kann. Ohne Einträge nur der Platzhalter.
|
||||
"""
|
||||
if not eintraege:
|
||||
return f"<p style='color:#888888;'>{html.escape(PLATZHALTER)}</p>"
|
||||
|
||||
bloecke = []
|
||||
for e in eintraege:
|
||||
name = " ".join(t for t in (e.anrede, e.titel, e.vorname, e.nachname) if t)
|
||||
ort = " ".join(t for t in (e.plz, e.wohnort) if t)
|
||||
anschrift = "<br/>".join(html.escape(t) for t in (e.strasse, ort, _ortsteil(e.ortsteil)) if t)
|
||||
email = html.escape(", ".join(e.emails)) if e.emails else f"<i>{OHNE_EMAIL}</i>"
|
||||
bloecke.append(
|
||||
"<table cellpadding='6'>"
|
||||
f"<tr><td style='color:#666666;'>Name</td><td><b>{html.escape(name)}</b></td></tr>"
|
||||
f"<tr><td style='color:#666666;' valign='top'>Anschrift</td><td>{anschrift}</td></tr>"
|
||||
f"<tr><td style='color:#666666;'>E-Mail</td><td>{email}</td></tr>"
|
||||
"</table>"
|
||||
)
|
||||
return "<hr/>".join(bloecke)
|
||||
|
||||
|
||||
class EigentuemerAnzeige(QWidget):
|
||||
"""Eigenes Fenster (nicht im Dock) mit großer Schrift für den Eigentümer.
|
||||
|
||||
``beim_schliessen`` wird aufgerufen, wenn das Fenster von außen geschlossen
|
||||
wird (z. B. Alt+F4), damit der Tab die Sitzung als beendet führt.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent, Qt.WindowType.Window)
|
||||
self.beim_schliessen: Optional[Callable[[], None]] = None
|
||||
self.setWindowTitle(PLATZHALTER)
|
||||
self.setStyleSheet("background-color: white; color: black;")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(60, 50, 60, 50)
|
||||
self._text = QLabel()
|
||||
self._text.setTextFormat(Qt.TextFormat.RichText)
|
||||
self._text.setWordWrap(True)
|
||||
self._text.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
|
||||
self._text.setStyleSheet("font-size: 26pt;")
|
||||
layout.addWidget(self._text)
|
||||
self.leeren()
|
||||
|
||||
def zeige(self, eintraege: Sequence[Eigentuemer]) -> None:
|
||||
"""Ersetzt den Inhalt durch die Einträge."""
|
||||
self._text.setText(anzeige_html(eintraege))
|
||||
|
||||
def leeren(self) -> None:
|
||||
"""Zeigt nur noch den neutralen Platzhalter."""
|
||||
self._text.setText(anzeige_html([]))
|
||||
|
||||
def closeEvent(self, event): # noqa: N802 (Qt-Name)
|
||||
rueckruf, self.beim_schliessen = self.beim_schliessen, None
|
||||
if rueckruf is not None:
|
||||
rueckruf()
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
def zeige_anzeigefenster(fenster, geometrie=None) -> None:
|
||||
"""Zeigt das Anzeigefenster als normales Fenster mit Rahmen, nie im Vollbild.
|
||||
|
||||
Mit ``geometrie`` (aus ``saveGeometry()`` der letzten Sitzung) erscheint es
|
||||
wieder dort, wohin der Bearbeiter es geschoben hatte; sonst in
|
||||
Standardgröße an der von Qt gewählten Stelle.
|
||||
"""
|
||||
wiederhergestellt = False
|
||||
if geometrie is not None:
|
||||
try:
|
||||
wiederhergestellt = bool(fenster.restoreGeometry(geometrie))
|
||||
except Exception:
|
||||
wiederhergestellt = False
|
||||
if not wiederhergestellt:
|
||||
fenster.resize(*STANDARDGROESSE)
|
||||
fenster.show()
|
||||
fenster.raise_()
|
||||
fenster.activateWindow()
|
||||
@@ -0,0 +1,79 @@
|
||||
"""sn_verfahrensgebiet/ui/eigentuemer_korrektur_dialog.py – Korrekturdialog für den Bearbeiter (#56).
|
||||
|
||||
Zeigt die Felder eines angezeigten Eintrags zum Bearbeiten. Sind mehrere
|
||||
Einträge angezeigt, wählt der Bearbeiter oben, welchen er korrigiert. Der
|
||||
Dialog speichert nichts selbst; das übernimmt der Tab über
|
||||
:mod:`sn_verfahrensgebiet.functions.eigentuemer_korrektur`.
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional, Sequence, Tuple
|
||||
|
||||
from sn_basis.functions.qt_wrapper import (
|
||||
QT_VERSION, exec_dialog, DialogAccepted,
|
||||
QDialog, QDialogButtonBox, QVBoxLayout, QGridLayout, QLabel, QLineEdit, QComboBox,
|
||||
)
|
||||
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_korrektur import FELDER, feldwerte
|
||||
|
||||
|
||||
def frage_korrektur(
|
||||
parent,
|
||||
eintraege: Sequence[Eigentuemer],
|
||||
) -> Optional[Tuple[int, Dict[str, str]]]:
|
||||
"""Fragt die korrigierten Werte ab.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Optional[Tuple[int, Dict[str, str]]]
|
||||
Index des gewählten Eintrags und die Werte aller Felder, oder ``None``
|
||||
bei Abbruch (und im Mock-Modus).
|
||||
"""
|
||||
if not eintraege or QT_VERSION == 0:
|
||||
return None
|
||||
|
||||
dialog = QDialog(parent)
|
||||
dialog.setWindowTitle("Daten korrigieren")
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
auswahl = QComboBox()
|
||||
for eintrag in eintraege:
|
||||
auswahl.addItem(f"{eintrag.name} – {eintrag.anschrift}")
|
||||
if len(eintraege) > 1:
|
||||
layout.addWidget(QLabel("Eintrag:"))
|
||||
layout.addWidget(auswahl)
|
||||
|
||||
raster = QGridLayout()
|
||||
felder: Dict[str, QLineEdit] = {}
|
||||
for zeile, (schluessel, beschriftung) in enumerate(FELDER):
|
||||
raster.addWidget(QLabel(f"{beschriftung}:"), zeile, 0)
|
||||
feld = QLineEdit()
|
||||
raster.addWidget(feld, zeile, 1)
|
||||
felder[schluessel] = feld
|
||||
layout.addLayout(raster)
|
||||
|
||||
def _fuellen(index: int) -> None:
|
||||
if 0 <= index < len(eintraege):
|
||||
for schluessel, wert in feldwerte(eintraege[index]).items():
|
||||
felder[schluessel].setText(wert)
|
||||
|
||||
auswahl.currentIndexChanged.connect(_fuellen)
|
||||
_fuellen(0)
|
||||
|
||||
hinweis = QLabel(
|
||||
"<small>Die Eigentümerdatei bleibt unverändert. Jede Änderung wird in der "
|
||||
"Korrektur-CSV festgehalten und sofort im Anzeigefenster gezeigt. "
|
||||
"Mehrere E-Mail-Adressen mit Komma trennen.</small>"
|
||||
)
|
||||
hinweis.setWordWrap(True)
|
||||
layout.addWidget(hinweis)
|
||||
|
||||
knoepfe = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel)
|
||||
knoepfe.accepted.connect(dialog.accept)
|
||||
knoepfe.rejected.connect(dialog.reject)
|
||||
layout.addWidget(knoepfe)
|
||||
dialog.resize(520, dialog.sizeHint().height())
|
||||
|
||||
if exec_dialog(dialog) != DialogAccepted:
|
||||
return None
|
||||
return auswahl.currentIndex(), {schluessel: feld.text() for schluessel, feld in felder.items()}
|
||||
@@ -0,0 +1,83 @@
|
||||
"""sn_verfahrensgebiet/ui/eigentuemer_treffer.py – Trefferliste der Eigentümersuche (#56).
|
||||
|
||||
Nicht modales Fenster für den Bearbeiter. Es zeigt die zusammengefassten
|
||||
Treffer mit dem Wert von ``PER_RECHTSPERSON`` (noch nicht entschlüsselt) und dem
|
||||
Kennzeichen „verstorben“ und erlaubt die Mehrfachauswahl. „Daten anzeigen“
|
||||
übergibt die Auswahl an das Anzeigefenster für den Eigentümer. Die Tabelle kommt aus
|
||||
:func:`~sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge.baue_auswahltabelle`,
|
||||
dieselbe wie bei „Verfahrensgebiete aus Landkreis laden“.
|
||||
"""
|
||||
|
||||
from typing import Callable, List, Optional, Sequence, Tuple
|
||||
|
||||
from sn_basis.functions.qt_wrapper import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
|
||||
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_suche import Eigentuemer
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_dialoge import (
|
||||
baue_auswahltabelle,
|
||||
ausgewaehlte_indizes,
|
||||
)
|
||||
|
||||
SPALTENKOEPFE = ["Name", "Anschrift", "E-Mail", "Rechtsperson", "Hinweis"]
|
||||
|
||||
|
||||
def trefferzeile(eigentuemer: Eigentuemer) -> Tuple[str, str, str, str, str]:
|
||||
"""Spaltentexte eines Treffers in der Reihenfolge von :data:`SPALTENKOEPFE`."""
|
||||
return (
|
||||
eigentuemer.name,
|
||||
eigentuemer.anschrift,
|
||||
", ".join(eigentuemer.emails),
|
||||
eigentuemer.rechtsperson_text,
|
||||
eigentuemer.hinweise,
|
||||
)
|
||||
|
||||
|
||||
class EigentuemerTrefferFenster(QDialog):
|
||||
"""Trefferliste der Eigentümersuche; bleibt offen, während weitergesucht wird."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
treffer: Sequence[Eigentuemer],
|
||||
parent=None,
|
||||
beim_anzeigen: Optional[Callable[[List[Eigentuemer]], None]] = None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.treffer: List[Eigentuemer] = list(treffer)
|
||||
self.beim_anzeigen = beim_anzeigen
|
||||
self.setWindowTitle("Eigentümersuche – Treffer")
|
||||
self.setModal(False)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
anzahl = len(self.treffer)
|
||||
layout.addWidget(QLabel("1 Eintrag gefunden." if anzahl == 1 else f"{anzahl} Einträge gefunden."))
|
||||
|
||||
self.tabelle = baue_auswahltabelle(
|
||||
layout,
|
||||
SPALTENKOEPFE,
|
||||
[trefferzeile(e) for e in self.treffer],
|
||||
platzhalter="Name, Ort oder Hinweis …",
|
||||
)
|
||||
self.tabelle.resizeColumnsToContents()
|
||||
if anzahl == 1:
|
||||
self.tabelle.selectRow(0)
|
||||
|
||||
knoepfe = QHBoxLayout()
|
||||
self.btn_anzeigen = QPushButton("Daten anzeigen")
|
||||
self.btn_anzeigen.setToolTip("Zeigt die ausgewählten Einträge im Fenster für den Eigentümer.")
|
||||
self.btn_anzeigen.clicked.connect(self._on_anzeigen)
|
||||
knoepfe.addWidget(self.btn_anzeigen)
|
||||
knoepfe.addStretch()
|
||||
self.btn_schliessen = QPushButton("Schließen")
|
||||
self.btn_schliessen.clicked.connect(self.close)
|
||||
knoepfe.addWidget(self.btn_schliessen)
|
||||
layout.addLayout(knoepfe)
|
||||
|
||||
self.resize(760, 420)
|
||||
|
||||
def ausgewaehlte_eigentuemer(self) -> List[Eigentuemer]:
|
||||
"""Die in der Tabelle ausgewählten Treffer, in der Reihenfolge der Trefferliste."""
|
||||
return [self.treffer[i] for i in ausgewaehlte_indizes(self.tabelle)]
|
||||
|
||||
def _on_anzeigen(self) -> None:
|
||||
if self.beim_anzeigen is not None:
|
||||
self.beim_anzeigen(self.ausgewaehlte_eigentuemer())
|
||||
@@ -0,0 +1,440 @@
|
||||
"""sn_verfahrensgebiet/ui/tabs/eigentuemer_tab.py – Tab „Eigentümer“ (#56).
|
||||
|
||||
Zwei Bereiche:
|
||||
|
||||
- **Flurstückssuche:** Gemarkung (Name oder Nummer) und Flurstücksnummer
|
||||
eingeben, „Suchen“ wählt die Treffer im Layer „Flurstücke“ aus und zoomt hin.
|
||||
Logik: :mod:`sn_verfahrensgebiet.functions.flurstueck_suche`.
|
||||
- **Eigentümersuche:** Eigentümerdatei (CSV-Layer) wählen, Name und/oder Vorname
|
||||
eingeben, „Suchen“ öffnet die Trefferliste.
|
||||
Logik: :mod:`sn_verfahrensgebiet.functions.eigentuemer_suche`.
|
||||
- **Anzeige für den Eigentümer:** Sitzung beginnen und beenden, Anzeige
|
||||
leeren, angezeigte Daten korrigieren. „Daten anzeigen“ in der Trefferliste
|
||||
füllt das Anzeigefenster (und beginnt bei Bedarf die Sitzung). Das Fenster
|
||||
schiebt der Bearbeiter selbst auf den Bildschirm des Eigentümers; Größe und
|
||||
Lage gelten für die nächste Sitzung weiter. Korrekturen landen in einer
|
||||
Korrektur-CSV neben der Eigentümerdatei, deren Pfad unter den Knöpfen steht
|
||||
(:mod:`sn_verfahrensgebiet.functions.eigentuemer_korrektur`).
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from sn_basis.functions.qt_wrapper import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QLineEdit, QPushButton, QComboBox,
|
||||
QFileDialog, Qt,
|
||||
)
|
||||
from sn_basis.functions.qgisui_wrapper import iface
|
||||
from sn_basis.functions.dialog_wrapper import show_info_dialog
|
||||
from sn_basis.functions.message_wrapper import success, info, warning
|
||||
|
||||
from sn_verfahrensgebiet.functions.flurstueck_suche import (
|
||||
flurstueck_suchen,
|
||||
FlurstueckSucheFehler,
|
||||
KeinFlurstueckslayer,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_suche import (
|
||||
eigentuemerlayer_im_projekt,
|
||||
hole_layer,
|
||||
suche_eigentuemer,
|
||||
EigentuemerSucheFehler,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.eigentuemer_korrektur import (
|
||||
ermittle_aenderungen,
|
||||
korrektur_pfad,
|
||||
schreibe_korrekturen,
|
||||
wende_an,
|
||||
)
|
||||
from sn_verfahrensgebiet.ui.eigentuemer_treffer import EigentuemerTrefferFenster
|
||||
from sn_verfahrensgebiet.ui.eigentuemer_anzeige import EigentuemerAnzeige, zeige_anzeigefenster
|
||||
from sn_verfahrensgebiet.ui.eigentuemer_korrektur_dialog import frage_korrektur
|
||||
|
||||
_TITEL_SUCHE = "Flurstückssuche"
|
||||
_TITEL_EIGENTUEMER = "Eigentümersuche"
|
||||
_TITEL_ANZEIGE = "Anzeige für den Eigentümer"
|
||||
|
||||
|
||||
class EigentuemerLayerAuswahl(QComboBox):
|
||||
"""Auswahl der Eigentümerdatei; zeigt nur Layer mit den Spalten Nachname und Vorname.
|
||||
|
||||
Die Liste wird beim Aufklappen neu aufgebaut, damit eine gerade geladene CSV
|
||||
ohne Projektsignale erscheint.
|
||||
"""
|
||||
|
||||
def aktualisieren(self) -> None:
|
||||
"""Baut die Liste neu auf und behält die bisherige Auswahl, solange es den Layer noch gibt."""
|
||||
bisher = self.currentData()
|
||||
self.clear()
|
||||
for layer in eigentuemerlayer_im_projekt():
|
||||
self.addItem(layer.name(), layer.id())
|
||||
index = self.findData(bisher) if bisher else -1
|
||||
self.setCurrentIndex(index if index >= 0 else 0)
|
||||
|
||||
def showPopup(self) -> None:
|
||||
self.aktualisieren()
|
||||
super().showPopup()
|
||||
|
||||
|
||||
class EigentuemerTab(QWidget):
|
||||
"""Tab für Flurstückssuche und Eigentümersuche.
|
||||
|
||||
Attributes:
|
||||
tab_title: Anzeigetitel des Tabs ("Eigentümer")
|
||||
"""
|
||||
|
||||
tab_title = "Eigentümer"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
layout.addWidget(QLabel("<b>Flurstückssuche</b>"))
|
||||
|
||||
raster = QGridLayout()
|
||||
raster.addWidget(QLabel("Gemarkung:"), 0, 0)
|
||||
self.gemarkung_edit = QLineEdit()
|
||||
self.gemarkung_edit.setPlaceholderText("Name oder Nummer, z. B. 0268")
|
||||
raster.addWidget(self.gemarkung_edit, 0, 1)
|
||||
raster.addWidget(QLabel("Flurstück:"), 1, 0)
|
||||
self.flurstueck_edit = QLineEdit()
|
||||
self.flurstueck_edit.setPlaceholderText("z. B. 562 oder 12/3")
|
||||
raster.addWidget(self.flurstueck_edit, 1, 1)
|
||||
layout.addLayout(raster)
|
||||
|
||||
zeile_suchen = QHBoxLayout()
|
||||
zeile_suchen.addStretch()
|
||||
self.btn_flurstueck_suchen = QPushButton("Suchen")
|
||||
self.btn_flurstueck_suchen.setToolTip(
|
||||
"Sucht im Layer „Flurstücke“. Ohne Nenner werden alle Flurstücke "
|
||||
"mit diesem Zähler gefunden."
|
||||
)
|
||||
zeile_suchen.addWidget(self.btn_flurstueck_suchen)
|
||||
layout.addLayout(zeile_suchen)
|
||||
|
||||
self.btn_flurstueck_suchen.clicked.connect(self._on_flurstueck_suchen)
|
||||
self.gemarkung_edit.returnPressed.connect(self._on_flurstueck_suchen)
|
||||
self.flurstueck_edit.returnPressed.connect(self._on_flurstueck_suchen)
|
||||
|
||||
titel_eigentuemer = QLabel("<b>Eigentümersuche</b>")
|
||||
titel_eigentuemer.setStyleSheet("margin-top: 12px;")
|
||||
layout.addWidget(titel_eigentuemer)
|
||||
|
||||
raster_eig = QGridLayout()
|
||||
raster_eig.addWidget(QLabel("Eigentümerdatei:"), 0, 0)
|
||||
self.eigentuemer_layer_combo = EigentuemerLayerAuswahl()
|
||||
self.eigentuemer_layer_combo.setToolTip(
|
||||
"CSV-Layer mit den Spalten Nachname und Vorname (Serienbriefexport). "
|
||||
"Die CSV von Hand ins Projekt laden; die Liste wird beim Aufklappen aktualisiert."
|
||||
)
|
||||
raster_eig.addWidget(self.eigentuemer_layer_combo, 0, 1)
|
||||
raster_eig.addWidget(QLabel("Name:"), 1, 0)
|
||||
self.nachname_edit = QLineEdit()
|
||||
raster_eig.addWidget(self.nachname_edit, 1, 1)
|
||||
raster_eig.addWidget(QLabel("Vorname:"), 2, 0)
|
||||
self.vorname_edit = QLineEdit()
|
||||
raster_eig.addWidget(self.vorname_edit, 2, 1)
|
||||
layout.addLayout(raster_eig)
|
||||
|
||||
zeile_eig_suchen = QHBoxLayout()
|
||||
zeile_eig_suchen.addStretch()
|
||||
self.btn_eigentuemer_suchen = QPushButton("Suchen")
|
||||
self.btn_eigentuemer_suchen.setToolTip(
|
||||
"Findet Einträge, deren Name und Vorname die Eingaben enthalten. "
|
||||
"Gleiche Personen mit gleicher Anschrift werden zusammengefasst."
|
||||
)
|
||||
zeile_eig_suchen.addWidget(self.btn_eigentuemer_suchen)
|
||||
layout.addLayout(zeile_eig_suchen)
|
||||
|
||||
self.btn_eigentuemer_suchen.clicked.connect(self._on_eigentuemer_suchen)
|
||||
self.nachname_edit.returnPressed.connect(self._on_eigentuemer_suchen)
|
||||
self.vorname_edit.returnPressed.connect(self._on_eigentuemer_suchen)
|
||||
|
||||
titel_anzeige = QLabel(f"<b>{_TITEL_ANZEIGE}</b>")
|
||||
titel_anzeige.setStyleSheet("margin-top: 12px;")
|
||||
layout.addWidget(titel_anzeige)
|
||||
|
||||
zeile_sitzung = QHBoxLayout()
|
||||
self.btn_sitzung = QPushButton("Sitzung beginnen")
|
||||
self.btn_sitzung.setToolTip(
|
||||
"Öffnet das Anzeigefenster. Es auf den Bildschirm des Eigentümers schieben; "
|
||||
"Größe und Lage gelten für die nächste Sitzung weiter."
|
||||
)
|
||||
self.btn_anzeige_leeren = QPushButton("Anzeige leeren")
|
||||
self.btn_anzeige_leeren.setToolTip(
|
||||
"Entfernt die Daten aus dem Anzeigefenster, ohne es zu schließen – "
|
||||
"vor dem nächsten Eigentümer."
|
||||
)
|
||||
self.btn_korrigieren = QPushButton("Korrigieren …")
|
||||
self.btn_korrigieren.setToolTip(
|
||||
"Angezeigte Daten korrigieren; die Änderung wird in der Korrektur-CSV festgehalten."
|
||||
)
|
||||
for knopf in (self.btn_sitzung, self.btn_anzeige_leeren, self.btn_korrigieren):
|
||||
zeile_sitzung.addWidget(knopf)
|
||||
layout.addLayout(zeile_sitzung)
|
||||
|
||||
self.sitzung_label = QLabel()
|
||||
self.sitzung_label.setWordWrap(True)
|
||||
layout.addWidget(self.sitzung_label)
|
||||
|
||||
self.korrektur_label = QLabel()
|
||||
self.korrektur_label.setWordWrap(True)
|
||||
self.korrektur_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
layout.addWidget(self.korrektur_label)
|
||||
|
||||
self.btn_sitzung.clicked.connect(self._on_sitzung_umschalten)
|
||||
self.btn_anzeige_leeren.clicked.connect(self._on_anzeige_leeren)
|
||||
self.btn_korrigieren.clicked.connect(self._on_korrigieren)
|
||||
self.eigentuemer_layer_combo.currentIndexChanged.connect(
|
||||
lambda _index: self._aktualisiere_korrektur_label()
|
||||
)
|
||||
|
||||
self._trefferfenster = None
|
||||
self._treffer_layer_id = None
|
||||
self._anzeige = None
|
||||
self._anzeige_geometrie = None
|
||||
self._sitzung_seit = None
|
||||
self._angezeigt = []
|
||||
self._anzeige_layer_id = None
|
||||
self._korrektur_pfade = {}
|
||||
self.eigentuemer_layer_combo.aktualisieren()
|
||||
self._aktualisiere_sitzungsstatus()
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def _on_flurstueck_suchen(self) -> None:
|
||||
"""Sucht das eingegebene Flurstück, wählt die Treffer aus und zoomt hin."""
|
||||
gemarkung = self.gemarkung_edit.text().strip()
|
||||
nummer = self.flurstueck_edit.text().strip()
|
||||
try:
|
||||
ergebnis = flurstueck_suchen(gemarkung, nummer)
|
||||
except KeinFlurstueckslayer as exc:
|
||||
show_info_dialog(_TITEL_SUCHE, str(exc), self)
|
||||
return
|
||||
except FlurstueckSucheFehler as exc:
|
||||
warning(_TITEL_SUCHE, str(exc))
|
||||
return
|
||||
|
||||
if not ergebnis.feature_ids:
|
||||
info(_TITEL_SUCHE, f"Flurstück {nummer} in Gemarkung {gemarkung} nicht gefunden.")
|
||||
return
|
||||
|
||||
ergebnis.layer.selectByIds(ergebnis.feature_ids)
|
||||
self._zoome_auf_auswahl(ergebnis.layer, ergebnis.feature_ids)
|
||||
anzahl = len(ergebnis.feature_ids)
|
||||
text = "1 Flurstück" if anzahl == 1 else f"{anzahl} Flurstücke"
|
||||
success(_TITEL_SUCHE, f"{text} im Layer „{ergebnis.layer.name()}“ ausgewählt.")
|
||||
|
||||
@staticmethod
|
||||
def _zoome_auf_auswahl(layer, feature_ids) -> bool:
|
||||
"""Zoomt auf die Auswahl und lässt die Treffer kurz aufblinken.
|
||||
|
||||
``zoomToSelected`` rechnet selbst in das KBS der Kartenansicht um.
|
||||
Gibt ``False`` zurück, wenn keine Kartenansicht verfügbar ist.
|
||||
"""
|
||||
try:
|
||||
canvas = iface.mapCanvas()
|
||||
canvas.zoomToSelected(layer)
|
||||
canvas.flashFeatureIds(layer, feature_ids)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _on_eigentuemer_suchen(self) -> None:
|
||||
"""Sucht in der gewählten Eigentümerdatei und zeigt die Trefferliste.
|
||||
|
||||
Meldungen enthalten bewusst keine Namen (Datenschutz).
|
||||
"""
|
||||
layer = hole_layer(self.eigentuemer_layer_combo.currentData())
|
||||
if layer is None:
|
||||
self.eigentuemer_layer_combo.aktualisieren()
|
||||
layer = hole_layer(self.eigentuemer_layer_combo.currentData())
|
||||
if layer is None:
|
||||
warning(
|
||||
_TITEL_EIGENTUEMER,
|
||||
"Keine Eigentümerdatei gewählt. Bitte den Serienbriefexport (CSV) "
|
||||
"als Layer laden und hier auswählen.",
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
treffer = suche_eigentuemer(layer, self.nachname_edit.text(), self.vorname_edit.text())
|
||||
except EigentuemerSucheFehler as exc:
|
||||
warning(_TITEL_EIGENTUEMER, str(exc))
|
||||
return
|
||||
|
||||
if not treffer:
|
||||
info(_TITEL_EIGENTUEMER, "Keine passenden Einträge gefunden.")
|
||||
return
|
||||
self._treffer_layer_id = layer.id()
|
||||
self._zeige_treffer(treffer)
|
||||
|
||||
def _zeige_treffer(self, treffer) -> None:
|
||||
"""Ersetzt eine offene Trefferliste durch die neue."""
|
||||
alt = getattr(self, "_trefferfenster", None)
|
||||
if alt is not None:
|
||||
alt.close()
|
||||
self._trefferfenster = EigentuemerTrefferFenster(
|
||||
treffer, self, beim_anzeigen=self._daten_anzeigen
|
||||
)
|
||||
self._trefferfenster.show()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Anzeige für den Eigentümer
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _on_sitzung_umschalten(self) -> None:
|
||||
if self._anzeige is None:
|
||||
self._sitzung_beginnen()
|
||||
else:
|
||||
self._sitzung_beenden()
|
||||
|
||||
def _sitzung_beginnen(self) -> None:
|
||||
"""Öffnet das Anzeigefenster leer, mit Rahmen, an der Stelle der letzten Sitzung."""
|
||||
anzeige = EigentuemerAnzeige(self)
|
||||
anzeige.beim_schliessen = self._anzeige_wurde_geschlossen
|
||||
zeige_anzeigefenster(anzeige, self._anzeige_geometrie)
|
||||
self._anzeige = anzeige
|
||||
self._sitzung_seit = datetime.now()
|
||||
self._angezeigt = []
|
||||
self._aktualisiere_sitzungsstatus()
|
||||
|
||||
def _sitzung_beenden(self) -> None:
|
||||
"""Schließt das Anzeigefenster; die Korrektur-CSV bleibt, wie sie ist."""
|
||||
anzeige = self._anzeige
|
||||
self._anzeige_wurde_geschlossen()
|
||||
if anzeige is not None:
|
||||
anzeige.beim_schliessen = None
|
||||
anzeige.close()
|
||||
|
||||
def _anzeige_wurde_geschlossen(self) -> None:
|
||||
if self._anzeige is not None:
|
||||
try:
|
||||
self._anzeige_geometrie = self._anzeige.saveGeometry()
|
||||
except Exception:
|
||||
pass
|
||||
self._anzeige = None
|
||||
self._sitzung_seit = None
|
||||
self._angezeigt = []
|
||||
self._aktualisiere_sitzungsstatus()
|
||||
|
||||
def _on_anzeige_leeren(self) -> None:
|
||||
"""Entfernt die Daten aus der Anzeige, das Fenster bleibt offen."""
|
||||
if self._anzeige is not None:
|
||||
self._anzeige.leeren()
|
||||
self._angezeigt = []
|
||||
self._aktualisiere_sitzungsstatus()
|
||||
|
||||
def _daten_anzeigen(self, eintraege) -> None:
|
||||
"""Rückruf der Trefferliste: zeigt die gewählten Einträge, beginnt bei Bedarf die Sitzung."""
|
||||
if not eintraege:
|
||||
warning(_TITEL_ANZEIGE, "Bitte in der Trefferliste mindestens einen Eintrag auswählen.")
|
||||
return
|
||||
if self._anzeige is None:
|
||||
self._sitzung_beginnen()
|
||||
self._angezeigt = list(eintraege)
|
||||
self._anzeige_layer_id = self._treffer_layer_id
|
||||
self._anzeige.zeige(self._angezeigt)
|
||||
self._aktualisiere_sitzungsstatus()
|
||||
|
||||
def _aktualisiere_sitzungsstatus(self) -> None:
|
||||
"""Knopftexte und Statuszeile; ohne Namen, damit der Tab nichts preisgibt."""
|
||||
aktiv = self._anzeige is not None
|
||||
self.btn_sitzung.setText("Sitzung beenden" if aktiv else "Sitzung beginnen")
|
||||
self.btn_anzeige_leeren.setEnabled(aktiv and bool(self._angezeigt))
|
||||
self.btn_korrigieren.setEnabled(aktiv and bool(self._angezeigt))
|
||||
if not aktiv:
|
||||
text = "Keine Sitzung. „Daten anzeigen“ in der Trefferliste beginnt sie automatisch."
|
||||
else:
|
||||
anzahl = len(self._angezeigt)
|
||||
inhalt = "leer" if not anzahl else ("1 Eintrag" if anzahl == 1 else f"{anzahl} Einträge")
|
||||
text = f"Sitzung seit {self._sitzung_seit:%H:%M} Uhr – Anzeige: {inhalt}"
|
||||
self.sitzung_label.setText(text)
|
||||
self._aktualisiere_korrektur_label()
|
||||
|
||||
def _korrektur_layer_id(self):
|
||||
"""Eigentümerdatei, zu der Korrekturen gehören: die angezeigte, sonst die gesuchte, sonst die gewählte."""
|
||||
return self._anzeige_layer_id or self._treffer_layer_id or self.eigentuemer_layer_combo.currentData()
|
||||
|
||||
def _korrektur_vorschau(self):
|
||||
"""Pfad der Korrektur-CSV ohne Rückfrage, oder ``None``, wenn er erst beim Speichern erfragt wird."""
|
||||
layer_id = self._korrektur_layer_id()
|
||||
if layer_id in self._korrektur_pfade:
|
||||
return self._korrektur_pfade[layer_id]
|
||||
layer = hole_layer(layer_id)
|
||||
vorschlag = korrektur_pfad(layer.source()) if layer is not None else None
|
||||
ordner = os.path.dirname(vorschlag) if vorschlag else ""
|
||||
if vorschlag and os.path.isdir(ordner) and os.access(ordner, os.W_OK):
|
||||
return vorschlag
|
||||
return None
|
||||
|
||||
def _aktualisiere_korrektur_label(self) -> None:
|
||||
"""Zeigt unter den Knöpfen, wohin Korrekturen geschrieben werden."""
|
||||
if not self._korrektur_layer_id():
|
||||
text = "Korrektur-Datei: – (keine Eigentümerdatei gewählt)"
|
||||
else:
|
||||
pfad = self._korrektur_vorschau()
|
||||
if pfad is None:
|
||||
text = "Korrektur-Datei: wird beim ersten Speichern abgefragt"
|
||||
else:
|
||||
zusatz = "" if os.path.exists(pfad) else " (wird beim ersten Speichern angelegt)"
|
||||
text = f"Korrektur-Datei: {os.path.normpath(pfad)}{zusatz}"
|
||||
self.korrektur_label.setText(text)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Korrekturen
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _on_korrigieren(self) -> None:
|
||||
"""Fragt Korrekturen ab, hält sie in der Korrektur-CSV fest und zeigt sie sofort an."""
|
||||
if not self._angezeigt:
|
||||
warning(_TITEL_ANZEIGE, "Es werden keine Daten angezeigt.")
|
||||
return
|
||||
ergebnis = frage_korrektur(self, self._angezeigt)
|
||||
if ergebnis is None:
|
||||
return
|
||||
index, werte = ergebnis
|
||||
eintrag = self._angezeigt[index]
|
||||
aenderungen = ermittle_aenderungen(eintrag, werte)
|
||||
if not aenderungen:
|
||||
info(_TITEL_ANZEIGE, "Keine Änderung.")
|
||||
return
|
||||
|
||||
pfad = self._korrektur_datei()
|
||||
if not pfad:
|
||||
return
|
||||
try:
|
||||
schreibe_korrekturen(pfad, eintrag, aenderungen)
|
||||
except OSError as exc:
|
||||
self._korrektur_pfade.pop(self._anzeige_layer_id, None)
|
||||
warning(_TITEL_ANZEIGE, f"Die Korrektur-CSV konnte nicht geschrieben werden: {exc.strerror or exc}")
|
||||
return
|
||||
|
||||
self._angezeigt[index] = wende_an(eintrag, aenderungen)
|
||||
if self._anzeige is not None:
|
||||
self._anzeige.zeige(self._angezeigt)
|
||||
self._aktualisiere_korrektur_label()
|
||||
anzahl = len(aenderungen)
|
||||
text = "1 Änderung" if anzahl == 1 else f"{anzahl} Änderungen"
|
||||
success(_TITEL_ANZEIGE, f"{text} in „{os.path.basename(pfad)}“ festgehalten.")
|
||||
|
||||
def _korrektur_datei(self):
|
||||
"""Pfad der Korrektur-CSV für die angezeigte Eigentümerdatei.
|
||||
|
||||
Vorgabe ist ``<Eigentümerdatei>_korrekturen.csv`` daneben. Lässt sich
|
||||
daraus kein beschreibbarer Ordner ableiten, wird gefragt. Die Wahl gilt
|
||||
für die Eigentümerdatei, solange der Tab besteht.
|
||||
"""
|
||||
layer_id = self._anzeige_layer_id
|
||||
pfad = self._korrektur_vorschau() if layer_id == self._korrektur_layer_id() else None
|
||||
if pfad is None:
|
||||
layer = hole_layer(layer_id)
|
||||
vorschlag = korrektur_pfad(layer.source()) if layer is not None else None
|
||||
pfad, _ = QFileDialog.getSaveFileName(
|
||||
self, "Korrektur-CSV wählen", vorschlag or os.path.expanduser("~"), "CSV-Dateien (*.csv)"
|
||||
)
|
||||
if not pfad:
|
||||
return None
|
||||
self._korrektur_pfade[layer_id] = pfad
|
||||
return pfad
|
||||
+31
-83
@@ -5,7 +5,7 @@ Verwaltet die Verfahrensgebiet-Abgrenzung und ALKIS-Integration.
|
||||
from sn_basis.functions.qt_wrapper import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QMessageBox
|
||||
from sn_basis.functions.qt_wrapper import Qt
|
||||
from sn_basis.functions import Qgis, QgsProject #QgsMessageLog
|
||||
from sn_basis.functions.qgisui_wrapper import QgsMapLayerComboBox
|
||||
from sn_basis.functions.qgisui_wrapper import QgsMapLayerComboBox, zoom_auf_layer
|
||||
from sn_basis.functions.qgiscore_wrapper import QgsMapLayerProxyModel
|
||||
from sn_basis.functions.verfahrensgebiet_manager import get_verfahrensgebiet_layer_id, set_verfahrensgebiet_layer_id
|
||||
from sn_basis.functions.dialog_wrapper import show_info_dialog, create_progress_dialog, ask_yes_no
|
||||
@@ -15,12 +15,7 @@ from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||
from qgis.utils import iface
|
||||
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis import LoadStatus
|
||||
from sn_verfahrensgebiet.functions.flurstueck_alkis import (
|
||||
flurstueck_alkis,
|
||||
lade_flurstuecke_per_gemarkung,
|
||||
ALKIS_SF_URL,
|
||||
FLST_LAYER_NAME,
|
||||
)
|
||||
from sn_verfahrensgebiet.functions.flurstueck_alkis import flurstueck_alkis
|
||||
from sn_verfahrensgebiet.functions.verfahrensgebiet_alkis_komplett import (
|
||||
verfahrensgebiet_alkis_komplett,
|
||||
BROR_LAYER_NAME,
|
||||
@@ -230,8 +225,8 @@ class WorkingTab(QWidget):
|
||||
if not project.mapLayer(enriched_layer.id()):
|
||||
project.addMapLayer(enriched_layer)
|
||||
apply_style(enriched_layer, "verfahrensgebiet.qml")
|
||||
iface.mapCanvas().setExtent(enriched_layer.extent())
|
||||
iface.mapCanvas().refresh()
|
||||
# Ausdehnung ins KBS der Kartenansicht umrechnen (Basis #99)
|
||||
zoom_auf_layer(enriched_layer, iface.mapCanvas())
|
||||
self.setze_haken(self.haken_verf, True)
|
||||
|
||||
self.verf_layer_combo.setLayer(enriched_layer)
|
||||
@@ -287,8 +282,8 @@ class WorkingTab(QWidget):
|
||||
if not project.mapLayer(enriched_layer.id()):
|
||||
project.addMapLayer(enriched_layer)
|
||||
apply_style(enriched_layer, "verfahrensgebiet.qml")
|
||||
iface.mapCanvas().setExtent(enriched_layer.extent())
|
||||
iface.mapCanvas().refresh()
|
||||
# Ausdehnung ins KBS der Kartenansicht umrechnen (Basis #99)
|
||||
zoom_auf_layer(enriched_layer, iface.mapCanvas())
|
||||
self.setze_haken(self.haken_verf, True)
|
||||
|
||||
self.verf_layer_combo.setLayer(enriched_layer)
|
||||
@@ -392,6 +387,25 @@ class WorkingTab(QWidget):
|
||||
# Flurstücke-Handler
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _uebernimm_flurstuecke_ergebnis(self, layer, status) -> None:
|
||||
"""Übernimmt das Ergebnis von flurstueck_alkis() ins Projekt (FIRST/RELOAD/APPEND/KEEP)."""
|
||||
if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
|
||||
QgsProject.instance().addMapLayer(layer)
|
||||
apply_style(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
|
||||
iface.mapCanvas().refresh()
|
||||
self.setze_haken(self.haken_flurst, True)
|
||||
msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen"
|
||||
success("Flurstücke", f"Layer wurde {msg}.", duration=5)
|
||||
|
||||
elif status == LoadStatus.KEEP:
|
||||
self.setze_haken(self.haken_flurst, True)
|
||||
info("Flurstücke", "Vorhandener Layer wurde behalten.", duration=4)
|
||||
|
||||
elif status == LoadStatus.APPEND:
|
||||
self.setze_haken(self.haken_flurst, True)
|
||||
iface.mapCanvas().refresh()
|
||||
success("Flurstücke", "Features wurden an den vorhandenen Layer angehängt.", duration=5)
|
||||
|
||||
def handle_flurst_alkis(self) -> None:
|
||||
"""Lädt Flurstücke aus ALKIS-WFS anhand von Gemarkungsschlüsseln."""
|
||||
self._set_busy(self.btn_flurst_alkis, True, "Aus ALKIS laden")
|
||||
@@ -399,25 +413,7 @@ class WorkingTab(QWidget):
|
||||
layer, status = flurstueck_alkis(self)
|
||||
if not layer or not layer.isValid():
|
||||
return
|
||||
|
||||
if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
|
||||
QgsProject.instance().addMapLayer(layer)
|
||||
# iface.mapCanvas().setExtent(layer.extent()) # Kartenausschnitt bleibt unverändert
|
||||
apply_style(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
|
||||
iface.mapCanvas().refresh()
|
||||
self.setze_haken(self.haken_flurst, True)
|
||||
msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen"
|
||||
success("Flurstücke", f"Layer wurde {msg}.", duration=5)
|
||||
|
||||
elif status == LoadStatus.KEEP:
|
||||
self.setze_haken(self.haken_flurst, True)
|
||||
info("Flurstücke", "Vorhandener Layer wurde behalten.", duration=4)
|
||||
|
||||
elif status == LoadStatus.APPEND:
|
||||
self.setze_haken(self.haken_flurst, True)
|
||||
iface.mapCanvas().refresh()
|
||||
success("Flurstücke", "Features wurden an den vorhandenen Layer angehängt.", duration=5)
|
||||
|
||||
self._uebernimm_flurstuecke_ergebnis(layer, status)
|
||||
finally:
|
||||
self._set_busy(self.btn_flurst_alkis, False, "Aus ALKIS laden")
|
||||
|
||||
@@ -491,68 +487,20 @@ class WorkingTab(QWidget):
|
||||
)
|
||||
return
|
||||
|
||||
# Prüfen ob bereits ein Flurstücke-Layer vorhanden → 3-Knopf-Dialog
|
||||
existing_flst = project.mapLayersByName(FLST_LAYER_NAME)
|
||||
if existing_flst:
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setWindowTitle("Layer bereits vorhanden")
|
||||
msg_box.setText(
|
||||
f"Der Layer '{FLST_LAYER_NAME}' existiert bereits.\nWas soll geschehen?"
|
||||
)
|
||||
btn_ueberschreiben = msg_box.addButton("Überschreiben", QMessageBox.ButtonRole.DestructiveRole)
|
||||
btn_anhaengen = msg_box.addButton("Anhängen", QMessageBox.ButtonRole.AcceptRole)
|
||||
btn_abbrechen = msg_box.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
|
||||
msg_box.setDefaultButton(btn_abbrechen)
|
||||
msg_box.exec()
|
||||
clicked = msg_box.clickedButton()
|
||||
if clicked is btn_abbrechen:
|
||||
return
|
||||
if clicked is btn_anhaengen:
|
||||
temp_layer = lade_flurstuecke_per_gemarkung(vorgabe_schluessel)
|
||||
if not temp_layer or not temp_layer.isValid() or temp_layer.featureCount() == 0:
|
||||
error(
|
||||
"Flurstücke",
|
||||
f"Flurstücke konnten nicht geladen werden – WFS-Dienst ({ALKIS_SF_URL}) "
|
||||
f"nicht erreichbar oder keine Features für die gewählten Gemarkungen.",
|
||||
duration=0,
|
||||
)
|
||||
return
|
||||
existing_flst[0].dataProvider().addFeatures(list(temp_layer.getFeatures()))
|
||||
existing_flst[0].updateExtents()
|
||||
iface.mapCanvas().refresh()
|
||||
self.setze_haken(self.haken_flurst, True)
|
||||
success("Flurstücke", "Features wurden an den vorhandenen Layer angehängt.", duration=5)
|
||||
return
|
||||
# else: Überschreiben → fall-through zum flurstueck_alkis(ohne_dialog=True)-Aufruf
|
||||
|
||||
# Flurstücke direkt laden (kein Dialog)
|
||||
# Flurstücke laden – Schlüssel bereits bekannt, aber bei bestehendem Layer
|
||||
# trotzdem interaktiv nachfragen (Überschreiben/Anhängen/Abbrechen, inkl. Dedup)
|
||||
layer, status = flurstueck_alkis(
|
||||
self,
|
||||
vorgabe_schluessel=vorgabe_schluessel,
|
||||
vorgabe_namen=vorgabe_namen,
|
||||
ohne_dialog=True,
|
||||
ohne_schluessel_abfrage=True,
|
||||
)
|
||||
|
||||
if not layer or not layer.isValid():
|
||||
error(
|
||||
"Flurstücke",
|
||||
f"Flurstücke konnten nicht geladen werden – WFS-Dienst ({ALKIS_SF_URL}) "
|
||||
f"nicht erreichbar oder keine Features für die gewählten Gemarkungen.",
|
||||
duration=0,
|
||||
)
|
||||
# flurstueck_alkis() hat bei Abbruch/Fehler bereits selbst eine Meldung gezeigt.
|
||||
return
|
||||
|
||||
if status in (LoadStatus.FIRST, LoadStatus.RELOAD):
|
||||
project.addMapLayer(layer)
|
||||
apply_style(layer, "GIS_Flst_Beschriftung_ALKIS_NAS.qml")
|
||||
iface.mapCanvas().refresh()
|
||||
self.setze_haken(self.haken_flurst, True)
|
||||
msg = "erstmalig geladen" if status == LoadStatus.FIRST else "neu geladen"
|
||||
success("Flurstücke", f"Layer wurde {msg}.", duration=5)
|
||||
|
||||
elif status == LoadStatus.KEEP:
|
||||
self.setze_haken(self.haken_flurst, True)
|
||||
info("Flurstücke", "Vorhandener Layer wurde behalten.", duration=4)
|
||||
self._uebernimm_flurstuecke_ergebnis(layer, status)
|
||||
|
||||
finally:
|
||||
self._set_busy(self.btn_flurst_aus_gemarkungen, False, "aus gewählten Gemarkungen laden")
|
||||
|
||||
Reference in New Issue
Block a user