Compare commits

..
6 changed files with 341 additions and 153 deletions
+6
View File
@@ -1,4 +1,10 @@
---
Version 26.04.1-unstable:
---
Version 26.3.3-unstable:
---
Version 26.3.2-unstable:
---
Version 26.3.1-testing:
- unstable als testing veröffentlicht
---
+2 -2
View File
@@ -1,11 +1,11 @@
[general]
version=26.3.1-testing
version=26.04.1-unstable
name=LNO Sachsen | Plan41
description=Plugin zum Erzeugen der Pläne nach §38 und §41
author=Daniel Helbig, Michael Otto
homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Plan41
tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Plan41/issues
repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_Test_Action/src/branch/testing/
repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_Test_Action/src/branch/unstable/
qgisMinimumVersion=3.40
experimental=true
-53
View File
@@ -94,59 +94,6 @@ class TestTabALogic(unittest.TestCase):
# -----------------------------------------------------
# 6. Layer-ID speichern
# -----------------------------------------------------
def test_verfahrensgebiet_layer_id_storage(self):
logic = TabALogic()
class MockLayer:
def id(self):
return "layer-123"
logic.save_verfahrensgebiet_layer(MockLayer())
stored = get_variable("verfahrensgebiet_layer", scope="project")
self.assertEqual(stored, "layer-123")
# -----------------------------------------------------
# 7. Ungültiger Layer wird ignoriert
# -----------------------------------------------------
def test_invalid_layer_is_rejected(self):
logic = TabALogic()
class InvalidLayer:
pass
logic.save_verfahrensgebiet_layer(InvalidLayer())
stored = get_variable("verfahrensgebiet_layer", scope="project")
self.assertEqual(stored, "")
#-----------------------------------------------------
# 8. Layer-ID wirft Exception
#----------------------------------------------------
def test_layer_id_raises_exception(self):
logic = TabALogic()
class BadLayer:
def id(self):
raise RuntimeError("boom")
logic.save_verfahrensgebiet_layer(BadLayer())
stored = get_variable("verfahrensgebiet_layer", scope="project")
self.assertEqual(stored, "")
# -----------------------------------------------------
# 11. Layer ID wird leer zurückgegeben
# -----------------------------------------------------
def test_layer_id_returns_empty(self):
logic = TabALogic()
class EmptyLayer:
def id(self):
return ""
logic.save_verfahrensgebiet_layer(EmptyLayer())
stored = get_variable("verfahrensgebiet_layer", scope="project")
self.assertEqual(stored, "")
if __name__ == "__main__":
+111
View File
@@ -0,0 +1,111 @@
import unittest
from unittest.mock import patch
from typing import Any, cast
from sn_plan41.ui.tab_a_logic import TabALogic
class _DummyPruefmanager:
def __init__(self) -> None:
self.hinweise: list[tuple[str, str]] = []
def verarbeite(self, ergebnis):
return ergebnis
def zeige_hinweis(self, titel: str, meldung: str) -> None:
self.hinweise.append((titel, meldung))
class _DummyDateiErgebnis:
def __init__(self, ok: bool = True, kontext: str = "C:/tmp/v_db.gpkg") -> None:
self.ok = ok
self.kontext = kontext
class _DummyProgress:
def __init__(self) -> None:
self.closed = False
def set_total(self, _value: int) -> None:
pass
def set_value(self, _value: int) -> None:
pass
def set_label(self, _value: str) -> None:
pass
def close(self) -> None:
self.closed = True
class _DummyProject:
def mapLayer(self, _layer_id: str):
return None
class _DummyGrabber:
pass
class TestTabAPreconditions(unittest.TestCase):
def _make_logic(self) -> tuple[TabALogic, _DummyPruefmanager]:
pruefmanager = _DummyPruefmanager()
logic = TabALogic(
pruefmanager=cast(Any, pruefmanager),
link_pruefer=cast(Any, object()),
stil_pruefer=cast(Any, object()),
)
return logic, pruefmanager
def test_get_verfahrensgebiet_layer_shows_hint_when_layer_id_missing(self):
logic, pruefmanager = self._make_logic()
with patch.object(logic, "load_verfahrensgebiet_layer_id", return_value=None):
layer = logic._get_verfahrensgebiet_layer()
self.assertIsNone(layer)
self.assertEqual(len(pruefmanager.hinweise), 1)
self.assertIn("Verfahrensgebiet", pruefmanager.hinweise[0][0])
@patch("sn_plan41.ui.tab_a_logic.QgsProject.instance", return_value=_DummyProject())
def test_get_verfahrensgebiet_layer_shows_hint_when_layer_not_in_project(self, _mock_project_instance):
logic, pruefmanager = self._make_logic()
with patch.object(logic, "load_verfahrensgebiet_layer_id", return_value="layer_404"):
layer = logic._get_verfahrensgebiet_layer()
self.assertIsNone(layer)
self.assertEqual(len(pruefmanager.hinweise), 1)
self.assertIn("Verfahrensgebiet", pruefmanager.hinweise[0][0])
@patch("sn_plan41.ui.tab_a_logic.Dateipruefer")
def test_pipeline_aborts_and_shows_hint_when_verfahrensgebiet_missing(
self,
mock_dateipruefer,
):
logic, pruefmanager = self._make_logic()
logic.data_grabber = cast(Any, _DummyGrabber())
mock_dateipruefer.return_value.pruefe.return_value = _DummyDateiErgebnis(ok=True, kontext="C:/tmp/v_db.gpkg")
progress = _DummyProgress()
with patch("sn_plan41.ui.tab_a_logic.create_progress_dialog", return_value=progress):
with patch.object(logic, "_resolve_linkliste", return_value="C:/tmp/linkliste.xlsx"):
with patch.object(logic, "load_verfahrensgebiet_layer_id", return_value=None):
result = logic._on_run_pipeline(
source="C:/tmp/v_db.gpkg",
linkliste=None,
raumfilter="Verfahrensgebiet",
)
self.assertIsNone(result)
self.assertEqual(len(pruefmanager.hinweise), 1)
self.assertIn("Verfahrensgebiet", pruefmanager.hinweise[0][0])
self.assertTrue(progress.closed)
if __name__ == "__main__":
unittest.main()
+221 -53
View File
@@ -12,6 +12,7 @@ import os
import datetime
import json
import tempfile
import html
from sn_basis.functions.qgiscore_wrapper import (
QgsVectorFileWriter,
@@ -63,9 +64,88 @@ class TabALogic:
self.link_pruefer = link_pruefer
self.stil_pruefer = stil_pruefer
self.data_grabber: Optional[DataGrabber] = None
class _SilentPruefmanagerProxy:
"""Proxy, der Prüfergebnisse ohne UI-Meldungen verarbeitet."""
def __init__(self, outer: "TabALogic", fallback: Optional[Any]) -> None:
self._outer = outer
self._fallback = fallback
self.results: List[Any] = []
def verarbeite(self, ergebnis: Any) -> Any:
self.results.append(ergebnis)
ok = bool(getattr(ergebnis, "ok", False))
if not ok:
aktion = getattr(ergebnis, "aktion", "unbekannt")
meldung = getattr(ergebnis, "meldung", "")
self._outer._log(f"[SILENT-PRUEFMANAGER] {aktion}: {meldung}")
return ergebnis
def __getattr__(self, name: str) -> Any:
if self._fallback is not None:
return getattr(self._fallback, name)
raise AttributeError(name)
def _log(self, msg: str) -> None:
print(f"[TabALogic] {msg}")
def _create_silent_pruefmanager(self) -> "TabALogic._SilentPruefmanagerProxy":
return TabALogic._SilentPruefmanagerProxy(self, self.pruefmanager)
def _extract_datenabruf_error_maps(self, datenabruf_results: List[Any]) -> Dict[str, Dict[str, str]]:
by_ident: Dict[str, str] = {}
by_thema: Dict[str, str] = {}
for result in datenabruf_results or []:
if not result:
continue
context = getattr(result, "kontext", None)
if not isinstance(context, dict):
continue
if getattr(result, "aktion", None) == "url_nicht_erreichbar":
ident = str(context.get("ident") or "").strip()
thema = str(context.get("thema") or "").strip()
reason = str(context.get("error") or getattr(result, "meldung", "") or "unbekannter Fehler").strip()
if ident and reason:
by_ident[ident] = reason
if thema and reason:
by_thema[thema] = reason
if getattr(result, "aktion", None) == "datenabruf":
fehler_map = context.get("fehler")
if isinstance(fehler_map, dict):
for thema_key, value in fehler_map.items():
thema = str(thema_key or "").strip()
reason = str(value or "").strip()
if thema and reason and thema not in by_thema:
by_thema[thema] = reason
return {"by_ident": by_ident, "by_thema": by_thema}
def _zeige_verfahrensgebiet_hinweis(self) -> None:
if not self.pruefmanager:
return
show_hint = getattr(self.pruefmanager, "zeige_hinweis", None)
if callable(show_hint):
show_hint(
"Verfahrensgebiet fehlt",
"Kein gueltiger Verfahrensgebiet-Layer vorhanden. "
"Bitte zuerst in sn_verfahrensgebiet den Layer ueber 'Aus ALKIS laden' laden "
"oder im Dropdown einen gueltigen Layer auswaehlen.",
)
def _close_progress_dialog(self, progress: Optional[Any]) -> None:
if progress is None:
return
close = getattr(progress, "close", None)
if callable(close):
try:
close()
except Exception:
pass
# -------------------------------
# Verfahrens-Datenbank (Pfad-Management)
# -------------------------------
@@ -155,12 +235,6 @@ class TabALogic:
# -------------------------------
# Verfahrensgebiet-Layer
# -------------------------------
def save_verfahrensgebiet_layer(self, layer: QgsVectorLayer) -> None:
"""Speichert die Verfahrensgebiet-Layer-ID, unter Annahme, dass der Layer prevalidiert ist."""
layer_id = layer.id() if layer is not None else ""
set_variable("verfahrensgebiet_layer", layer_id or "", scope="project")
def load_verfahrensgebiet_layer_id(self) -> Optional[str]:
value = get_variable("verfahrensgebiet_layer", scope="project")
return value or None
@@ -217,6 +291,7 @@ class TabALogic:
linkliste_final = self._resolve_linkliste(linkliste)
if linkliste_final is None:
self._log("Linkliste kann nicht aufgelöst werden")
self._close_progress_dialog(progress)
return None
else:
self._log(f"Linkliste final: '{linkliste_final}'")
@@ -224,6 +299,7 @@ class TabALogic:
raumfilter_layer = self._resolve_raumfilter(raumfilter, final_pfad)
if raumfilter in ("Verfahrensgebiet", "Pufferlayer") and raumfilter_layer is None:
self._log(f"Raumfilter '{raumfilter}' nicht verfügbar")
self._close_progress_dialog(progress)
return None
# 4) Lade-Status initialisieren (funktioniert ab Bestätigung überschreiben/anhängen)
@@ -236,7 +312,16 @@ class TabALogic:
progress.set_label("Prüfe Eingaben...")
# 5) Daten aus Linkliste laden und prüfen
source_dict, grabber_summary = self.data_grabber.run(linkliste_final)
silent_pruefmanager = self._create_silent_pruefmanager()
local_data_grabber = DataGrabber(
silent_pruefmanager,
datei_pruefer_cls=self.data_grabber._datei_pruefer_cls,
link_pruefer=self.data_grabber.link_pruefer,
layer_pruefer=self.data_grabber.layer_pruefer,
stil_pruefer=self.data_grabber.stil_pruefer,
excel_importer_cls=self.data_grabber._excel_importer_cls,
)
source_dict, grabber_summary = local_data_grabber.run(linkliste_final)
self._log(f"DataGrabber: {grabber_summary.meldung} [{grabber_summary.aktion}]")
# DEBUG: detaillierter Status
@@ -249,6 +334,7 @@ class TabALogic:
if not source_dict.get("rows"):
self._log("Keine validen Linkliste-Einträge für Datenabruf")
print("[TabALogic] ... STOP: rows:", len(source_dict.get("rows", [])))
self._close_progress_dialog(progress)
return None
total_rows = len(source_dict.get("rows", []))
@@ -266,7 +352,8 @@ class TabALogic:
self._log("Warnung: DataGrabber meldet fehlerhafte Zeilen, fahre mit Validierungsdaten fort")
# 5) Datenabruf (aus validierten Zeilen)
datenabruf = Datenabruf(self.pruefmanager)
silent_pruefmanager = self._create_silent_pruefmanager()
datenabruf = Datenabruf(silent_pruefmanager)
result_dict, datenabruf_results = datenabruf.datenabruf(
result_dict=source_dict,
raumfilter=raumfilter,
@@ -296,7 +383,7 @@ class TabALogic:
progress.set_label("Fachdaten laden abgeschlossen. Bitte OK klicken, um den Dialog zu schließen.")
# 7) Log-Datei schreiben
self._write_markdown_log(final_pfad, source_dict, pipeline_context, load_summary)
self._write_html_log(final_pfad, source_dict, pipeline_context, load_summary)
print("=" * 60 + "\n")
return pipeline_context
@@ -310,7 +397,15 @@ class TabALogic:
aborted = False
if not rows:
self._log("Keine Dienste zum Laden")
return
return {
"row_count": 0,
"loaded_count": 0,
"skipped_outside": 0,
"aborted": False,
"row_stats": [],
"layer_call_status": {},
"raumfilter_name": pipeline_context.get("raumfilter_name", "unbekannt"),
}
final_pfad = pipeline_context.get("source") or ""
use_datenschreiber = bool(final_pfad)
@@ -340,7 +435,12 @@ class TabALogic:
raumfilter_extent = raumfilter_layer.extent()
temp_layers: List[Any] = []
layer_loader = LayerLoader(self.pruefmanager, stil_pruefer=self.stil_pruefer, layer_pruefer=self.link_pruefer)
silent_pruefmanager = self._create_silent_pruefmanager()
layer_loader = LayerLoader(silent_pruefmanager, stil_pruefer=self.stil_pruefer, layer_pruefer=self.link_pruefer)
error_maps = self._extract_datenabruf_error_maps(pipeline_context.get("datenabruf_results", []))
error_by_ident = error_maps.get("by_ident", {})
error_by_thema = error_maps.get("by_thema", {})
# Statistiken für Log: Raumfilter-Info pro Dienst
row_stats: List[Dict[str, Any]] = []
@@ -357,6 +457,7 @@ class TabALogic:
daten_map = (pipeline_context.get("result_dict") or {}).get("daten", {})
fetched_features = daten_map.get(ident, []) if isinstance(daten_map, dict) else []
fetched_count = len(fetched_features) if isinstance(fetched_features, list) else None
base_reason = error_by_ident.get(ident) or error_by_thema.get(thema)
if progress is not None:
progress.set_label(f"Lade Dienst {idx}/{total}: {thema}")
@@ -385,6 +486,8 @@ class TabALogic:
if not layer:
layer_call_status[thema] = "layer_nicht_ladbar"
self._log(f"[DEBUG] Layeraufruf fehlgeschlagen: thema='{thema}'")
reason = base_reason or "Layer konnte nicht geladen werden (Dienst nicht erreichbar oder Link fehlerhaft)"
self._log(f"[DEBUG] Dienst nicht geladen: {thema} | Grund: {reason}")
row_stats.append({
"dienst": thema,
"provider": provider,
@@ -394,7 +497,7 @@ class TabALogic:
"total_features": None,
"filtered_features": None,
"status": "layer_nicht_ladbar",
"raumfilter": raumfilter_name,
"reason": reason,
})
continue
@@ -448,6 +551,7 @@ class TabALogic:
layer_call_status[thema] = "raumfilter_ausserhalb"
self._log(f"Dienst {thema} ist außerhalb des Raumfilters")
skipped_outside += 1
reason = "Keine Objekte innerhalb des Raumfilters"
row_stats.append({
"dienst": thema,
"provider": provider,
@@ -457,7 +561,7 @@ class TabALogic:
"total_features": total_features,
"filtered_features": 0,
"status": "außerhalb",
"raumfilter": raumfilter_name,
"reason": reason,
})
continue
@@ -473,7 +577,7 @@ class TabALogic:
"total_features": total_features,
"filtered_features": filtered_features,
"status": "geladen",
"raumfilter": raumfilter_name,
"reason": "geladen",
})
layer_call_status[thema] = "geladen"
self._log(f"[DEBUG] Dienst geladen: thema='{thema}', provider='{provider}', filtered_features={filtered_features}")
@@ -671,75 +775,118 @@ class TabALogic:
self._log(f"[DEBUG] Fehler beim Erzeugen lokaler Featureschicht für {thema}: {exc}")
return None
def _write_markdown_log(
def _write_html_log(
self,
final_pfad: str,
source_dict: DataDict,
pipeline_context: Dict[str, Any],
load_summary: Dict[str, Any],
) -> None:
"""Schreibt den Pipeline-Log (Markdown)."""
"""Schreibt den Pipeline-Log als HTML-Datei."""
lines = [
"# Plan41 Fachdaten-Ladevorgang",
"",
f"**Datum**: {datetime.datetime.now().isoformat()}",
f"**Verfahrens-DB**: {final_pfad or 'temporär'}",
f"**Linkliste**: {pipeline_context.get('linkliste')}",
"",
"## Zusammenfassung",
"",
f"- **Zeilen gesamt**: {load_summary.get('row_count', 0)}",
f"- **Geladene Dienste**: {load_summary.get('loaded_count', 0)}",
f"- **Außerhalb Raumfilter**: {load_summary.get('skipped_outside', 0)}",
f"- **Abgebrochen**: {load_summary.get('aborted', False)}",
f"- **Raumfilter**: {load_summary.get('raumfilter_name', 'unbekannt')}",
f"- **Raumfilter-Typ**: {pipeline_context.get('raumfilter_name', 'unbekannt')}",
"",
"## Dienstliste",
"",
"| Dienst | Provider | Linkadresse | Aufrufstatus | Ergebnisstatus |",
"|---|---|---|---|---|",
"<!doctype html>",
"<html lang='de'>",
"<head>",
" <meta charset='utf-8'>",
" <title>Plan41 Fachdaten-Ladevorgang</title>",
" <style>",
" body { font-family: Arial, sans-serif; margin: 20px; color: #222; }",
" h1, h2 { margin-bottom: 8px; }",
" .meta p { margin: 4px 0; }",
" table { border-collapse: collapse; width: 100%; margin: 12px 0 24px; }",
" th, td { border: 1px solid #ccc; padding: 6px 8px; text-align: left; vertical-align: top; }",
" th { background: #f2f2f2; }",
" .ok { color: #1b5e20; font-weight: 600; }",
" .warn { color: #8d6e00; font-weight: 600; }",
" .err { color: #b71c1c; font-weight: 600; }",
" </style>",
"</head>",
"<body>",
"<h1>Plan41 Fachdaten-Ladevorgang</h1>",
"<div class='meta'>",
f" <p><strong>Datum:</strong> {datetime.datetime.now().isoformat()}</p>",
f" <p><strong>Verfahrens-DB:</strong> {final_pfad or 'temporär'}</p>",
f" <p><strong>Linkliste:</strong> {pipeline_context.get('linkliste')}</p>",
f" <p><strong>Raumfilter:</strong> {load_summary.get('raumfilter_name', 'unbekannt')}</p>",
"</div>",
"<h2>Zusammenfassung</h2>",
"<ul>",
f" <li>Zeilen gesamt: {load_summary.get('row_count', 0)}</li>",
f" <li>Geladene Dienste: {load_summary.get('loaded_count', 0)}</li>",
f" <li>Außerhalb Raumfilter: {load_summary.get('skipped_outside', 0)}</li>",
f" <li>Abgebrochen: {load_summary.get('aborted', False)}</li>",
"</ul>",
"<h2>Dienstliste</h2>",
"<table>",
"<thead><tr><th>Dienst</th><th>Provider</th><th>Linkadresse</th><th>Aufrufstatus</th></tr></thead>",
"<tbody>",
]
status_by_dienst = {
str(stat.get("dienst", "")): str(stat.get("status", "n/a"))
for stat in load_summary.get("row_stats", [])
}
aufrufstatus_by_dienst = {
str(key): str(value)
for key, value in (load_summary.get("layer_call_status", {}) or {}).items()
}
aufrufstatus_labels = {
"layeraufruf_start": "Layer-Aufruf gestartet",
"layeraufruf_ok": "Layer-Aufruf erfolgreich",
"geladen": "Geladen",
"layer_nicht_ladbar": "Nicht ladbar",
"raumfilter_ausserhalb": "Außerhalb Raumfilter",
"abbruch_vor_layeraufruf": "Abbruch vor Layer-Aufruf",
"abbruch_nach_layeraufruf": "Abbruch nach Layer-Aufruf",
"abbruch_nach_raumfilter": "Abbruch nach Raumfilter",
"nicht_aufgerufen": "Nicht aufgerufen",
}
status_labels = {
"geladen": "Geladen",
"außerhalb": "Außerhalb Raumfilter",
"layer_nicht_ladbar": "Nicht ladbar",
}
for row in source_dict.get('rows', []):
dienst = row.get('Inhalt') or row.get('ident') or ''
provider = row.get('Provider') or ''
link = row.get('Link') or ''
aufrufstatus = aufrufstatus_by_dienst.get(str(dienst), "nicht_aufgerufen")
ergebnisstatus = status_by_dienst.get(str(dienst), "n/a")
lines.append(f"| {dienst} | {provider} | {link} | {aufrufstatus} | {ergebnisstatus} |")
aufrufstatus_text = aufrufstatus_labels.get(str(aufrufstatus), str(aufrufstatus))
lines.append(
f"<tr><td>{html.escape(str(dienst))}</td><td>{html.escape(str(provider))}</td><td>{html.escape(str(link))}</td><td>{html.escape(str(aufrufstatus_text))}</td></tr>"
)
lines.extend([
"",
"## Raumfilter-Statistik",
"",
"| Dienst | Provider | Linkadresse | Datenabruf-Objekte | Gesamt-Objekte | Gefilterte Objekte | Raumfilter | Status |",
"|---|---|---|---|---|---|---|---|",
"</tbody>",
"</table>",
"<h2>Raumfilter-Statistik</h2>",
"<table>",
"<thead><tr><th>Dienst</th><th>Datenabruf-Objekte</th><th>Gesamt-Objekte</th><th>Gefilterte Objekte</th><th>Status</th></tr></thead>",
"<tbody>",
])
for stat in load_summary.get('row_stats', []):
status = str(stat.get('status', 'n/a'))
status_label = status_labels.get(status, status)
reason = str(stat.get('reason', '') or '')
status_text = status_label if not reason else f"{status_label}: {reason}"
lines.append(
f"| {stat.get('dienst', '')} | {stat.get('provider', '')} | {stat.get('link', '')} | {stat.get('datenabruf_features', 'n/a')} | {stat.get('total_features', 'n/a')} | {stat.get('filtered_features', 'n/a')} | {stat.get('raumfilter', '')} | {stat.get('status', 'n/a')} |"
f"<tr><td>{html.escape(str(stat.get('dienst', '')))}</td><td>{html.escape(str(stat.get('datenabruf_features', 'n/a')))}</td><td>{html.escape(str(stat.get('total_features', 'n/a')))}</td><td>{html.escape(str(stat.get('filtered_features', 'n/a')))}</td><td>{html.escape(status_text)}</td></tr>"
)
markdown = "\n".join(lines)
lines.extend([
"</tbody>",
"</table>",
"</body>",
"</html>",
])
html_text = "\n".join(lines)
if final_pfad:
log_dir = os.path.dirname(final_pfad)
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, "plan41_lade_log.md")
log_file = os.path.join(log_dir, "plan41_lade_log.html")
try:
with open(log_file, "w", encoding="utf-8") as fh:
fh.write(markdown)
fh.write(html_text)
self.pruefmanager.verarbeite(
pruef_ergebnis(
@@ -754,15 +901,33 @@ class TabALogic:
self.pruefmanager.verarbeite(
pruef_ergebnis(
ok=False,
meldung=f"Fehler beim Schreiben des Logle (md): {exc}",
meldung=f"Fehler beim Schreiben des Logs (html): {exc}",
aktion="log_schreiben_fehlgeschlagen",
kontext={"error": str(exc)},
)
)
warning("Lade-Log", f"Konnte Datei nicht schreiben: {exc}", duration=10)
else:
# temporärer Modus: nur anzeigen
info("Lade-Log (temporär)", markdown, duration=20)
try:
tmp_dir = os.path.join(tempfile.gettempdir(), "sn_plan41")
os.makedirs(tmp_dir, exist_ok=True)
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
log_file = os.path.join(tmp_dir, f"plan41_lade_log_{timestamp}.html")
with open(log_file, "w", encoding="utf-8") as fh:
fh.write(html_text)
self._log(f"Lade-Log (temporär) gespeichert: {log_file}")
if hasattr(os, "startfile"):
try:
os.startfile(log_file)
except Exception as exc:
self._log(f"Konnte temporäre Log-Datei nicht öffnen: {exc}")
info("Lade-Log (temporär)", f"Lade-Protokoll gespeichert: {log_file}", duration=20)
except Exception as exc:
warning("Lade-Log", f"Konnte temporäre Log-Datei nicht schreiben: {exc}", duration=10)
def _clone_layer_with_extent(self, layer: QgsVectorLayer, extent, thema: str) -> QgsVectorLayer | None:
@@ -875,15 +1040,18 @@ class TabALogic:
if not layer_id:
self._log("❌ Keine Layer-ID gespeichert")
self._zeige_verfahrensgebiet_hinweis()
return None
layer = QgsProject.instance().mapLayer(layer_id)
if not layer:
self._log("❌ Layer-ID existiert nicht im Projekt")
self._zeige_verfahrensgebiet_hinweis()
return None
if not self.is_valid_verfahrensgebiet_layer(layer):
self._log("❌ Layer ist kein gültiger Vektorlayer")
self._zeige_verfahrensgebiet_hinweis()
return None
self._log(f"Verfahrensgebiet-Layer OK: '{layer.name()}'")
+1 -45
View File
@@ -166,16 +166,7 @@ class TabA(QWidget):
self.optional_content.setVisible(False)
main_layout.addWidget(self.optional_content)
# === LAYER-AUSWAHL + RAUMFILTER ===
layer_label = QLabel("Verfahrensgebiet-Layer auswählen")
layer_label.setStyleSheet("font-weight: bold; margin-top: 6px;")
main_layout.addWidget(layer_label)
self.layer_combo = QgsMapLayerComboBox()
self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer)
self.layer_combo.layerChanged.connect(self._on_layer_changed)
main_layout.addWidget(self.layer_combo)
# === RAUMFILTER ===
main_layout.addWidget(QLabel("Raumfilter"))
self._raumfilter_combo = QComboBox(self)
self._raumfilter_combo.setToolTip("Wählt die räumliche Bezugsfläche für die Datenextraktion.")
@@ -213,18 +204,6 @@ class TabA(QWidget):
self.lokale_linkliste = link
except Exception:
pass
# Layer
try:
layer_id = get_variable("tab_a_layer_id", scope="project")
if layer_id:
layer = QgsProject.instance().mapLayer(layer_id)
if layer and self.layer_combo:
self.layer_combo.setLayer(layer)
self._pufferlayer = layer
except Exception:
pass
# Raumfilter (schon im _build_ui behandelt)
# === UI CALLBACKS ===
@@ -249,20 +228,6 @@ class TabA(QWidget):
self.lokale_linkliste = path
set_variable("tab_a_linkliste", path, scope="project")
def _on_layer_changed(self, layer) -> None:
"""Persistiert Layer-Auswahl und registriert Verfahrensgebiet."""
self._pufferlayer = layer
if not layer:
return
# UI-State speichern
set_variable("tab_a_layer_id", layer.id(), scope="project")
# 🔹 NEU: Verfahrensgebiet explizit registrieren
if self.logic:
self.logic.save_verfahrensgebiet_layer(layer)
def _on_raumfilter_changed(self, value: str) -> None:
"""Persistieret Raumfilter-Auswahl."""
@@ -296,15 +261,6 @@ class TabA(QWidget):
source = self.file_widget.filePath()
raumfilter = self._raumfilter_combo.currentText()
linkliste = self.linkliste_widget.filePath()
if self.logic and self.layer_combo:
layer = self.layer_combo.currentLayer()
else:
layer = None
if layer and layer.name() == "Verfahrensgebiet":
self.logic.save_verfahrensgebiet_layer(layer)
if self.logic:
try:
self.logic._on_run_pipeline(source, linkliste, raumfilter)