Befund aus dem QGIS-Test: Die Bildschirmwahl wirkte nicht, das Fenster erschien immer auf dem Hauptbildschirm. Im Vollbild ließ es sich weder verschieben noch schließen und verdeckte QGIS; nur ein hartes Beenden von QGIS half. Bildschirmwahl und Vollbild entfallen. Das Anzeigefenster öffnet als normales Fenster mit Rahmen; der Bearbeiter schiebt es auf den Bildschirm des Eigentümers und kann es dort maximieren. Größe und Lage werden beim Beenden gemerkt und in der nächsten Sitzung wiederhergestellt (bis zum Neustart von QGIS). Unter den Knöpfen steht jetzt Pfad und Name der Korrektur-Datei, mit Hinweis, solange sie noch nicht angelegt ist oder erst abgefragt wird. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
441 lines
18 KiB
Python
441 lines
18 KiB
Python
"""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
|