Compare commits
35 Commits
439de5527a
...
unstable
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ff465b86d | |||
| f19fe71bfa | |||
| ae5f88c5b8 | |||
| 7cd6e3ef24 | |||
| 1be1420f66 | |||
| f25e30c489 | |||
| 0eb32453d6 | |||
| 841b529ad8 | |||
| ae5725cd03 | |||
| ac5a3993c8 | |||
| 22b45fe19a | |||
|
|
24c2137dc2 | ||
|
|
c0c0387b1d | ||
|
|
663ca770a1 | ||
|
|
04319b6f7b | ||
|
|
1c70d62739 | ||
|
|
3971bd3408 | ||
|
|
fa04fc80e3 | ||
|
|
04bdfbe9d8 | ||
|
|
b6b791e5bd | ||
|
|
82be564c29 | ||
|
|
f42260b66c | ||
|
|
327c25388f | ||
|
|
c6c9613120 | ||
| 6e1f4c615b | |||
| f876218134 | |||
| 9829ac9c81 | |||
| ae956b0046 | |||
| 0ec24029d8 | |||
| 26f426dfcd | |||
| 5dc8412a6a | |||
|
|
00f800b1e6 | ||
| 948041da52 | |||
| 3b56725e4f | |||
| 137baaf19c |
133
.gitea/workflows/release.yaml
Normal file
133
.gitea/workflows/release.yaml
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
name: Release Plugin
|
||||||
|
run-name: "Release | ${{ github.ref_name }}"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: alpine-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Notwendige Abhängigkeiten installieren
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
apk add --no-cache bash git jq curl
|
||||||
|
|
||||||
|
- name: Code holen
|
||||||
|
run: |
|
||||||
|
REPO_URL="https://${RELEASE_TOKEN}:x-oauth-basic@${{ vars.RELEASE_URL }}/${GITHUB_REPOSITORY}.git"
|
||||||
|
|
||||||
|
git clone "$REPO_URL" repo
|
||||||
|
cd repo
|
||||||
|
|
||||||
|
git checkout "$TAG"
|
||||||
|
env:
|
||||||
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
TAG: "${{ github.ref_name }}"
|
||||||
|
|
||||||
|
- name: Version und Kanal bestimmen
|
||||||
|
id: releaseinfo
|
||||||
|
run: |
|
||||||
|
TAG="${{ github.ref_name }}"
|
||||||
|
RAW_VERSION="${TAG#v}"
|
||||||
|
VERSION="${RAW_VERSION%%-*}"
|
||||||
|
|
||||||
|
# Channel und Suffix automatisch bestimmen anhand des Tag-Suffix
|
||||||
|
case "$RAW_VERSION" in
|
||||||
|
*-testing*|*-t|*-T)
|
||||||
|
CHANNEL="testing"
|
||||||
|
PRERELEASE="true"
|
||||||
|
SUFFIX="-testing"
|
||||||
|
;;
|
||||||
|
*-unstable*|*-u|*-U)
|
||||||
|
CHANNEL="unstable"
|
||||||
|
PRERELEASE="true"
|
||||||
|
SUFFIX="-unstable"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
CHANNEL="stable"
|
||||||
|
PRERELEASE="false"
|
||||||
|
SUFFIX=""
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Output setzen
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "channel=$CHANNEL" >> $GITHUB_OUTPUT
|
||||||
|
echo "prerelease=$PRERELEASE" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Optional Debug
|
||||||
|
echo "VERSION=$VERSION"
|
||||||
|
echo "CHANNEL=$CHANNEL"
|
||||||
|
echo "PRERELEASE=$PRERELEASE"
|
||||||
|
|
||||||
|
# - name: plugin.cfg einlesen
|
||||||
|
# id: config
|
||||||
|
# run: |
|
||||||
|
# cd repo
|
||||||
|
# while read -r line || [ -n "$line" ]; do
|
||||||
|
# key="${line%%=*}"
|
||||||
|
# value="${line#*=}"
|
||||||
|
# echo "$key=$value" >> $GITHUB_OUTPUT
|
||||||
|
# done < plugin.cfg
|
||||||
|
|
||||||
|
- name: Payload erzeugen
|
||||||
|
id: payload
|
||||||
|
run: |
|
||||||
|
cd repo
|
||||||
|
|
||||||
|
NAME="${GITHUB_REPOSITORY##*/}"
|
||||||
|
GROUP="${GITHUB_REPOSITORY%%/*}"
|
||||||
|
VERSION="${{ steps.releaseinfo.outputs.version }}"
|
||||||
|
CHANNEL="${{ steps.releaseinfo.outputs.channel }}"
|
||||||
|
PRERELEASE="${{ steps.releaseinfo.outputs.prerelease }}"
|
||||||
|
ZIP_FOLDER="${{ vars.ZIP_FOLDER }}"
|
||||||
|
ZIP_FILE="${ZIP_FOLDER}.zip"
|
||||||
|
TAG="${{ github.ref_name }}"
|
||||||
|
#GIT_URL=${GITHUB_REPOSITORY}
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--arg name "$NAME" \
|
||||||
|
--arg group "$GROUP" \
|
||||||
|
--arg version "$VERSION" \
|
||||||
|
--arg channel "$CHANNEL" \
|
||||||
|
--arg prerelease "$PRERELEASE" \
|
||||||
|
--arg zip_folder "$ZIP_FOLDER" \
|
||||||
|
--arg zip_file "$ZIP_FILE" \
|
||||||
|
--arg tag "$TAG" \
|
||||||
|
'{
|
||||||
|
name: $name,
|
||||||
|
group: $group,
|
||||||
|
version: $version,
|
||||||
|
channel: $channel,
|
||||||
|
prerelease: ($prerelease == "true"),
|
||||||
|
zip_folder: $zip_folder,
|
||||||
|
zip_file: $zip_file,
|
||||||
|
tag: $tag
|
||||||
|
}' > payload.json
|
||||||
|
|
||||||
|
cat payload.json
|
||||||
|
|
||||||
|
- name: Repository aktualisieren
|
||||||
|
run: |
|
||||||
|
NAME="${GITHUB_REPOSITORY##*/}"
|
||||||
|
TAG="${{ steps.releaseinfo.outputs.version }}"-"${{ steps.releaseinfo.outputs.channel }}"
|
||||||
|
PAYLOAD_B64=$(base64 -w0 repo/payload.json)
|
||||||
|
|
||||||
|
JSON="{\"ref\":\"hidden/workflows\",\"inputs\":{\"payload\":\"$PAYLOAD_B64\",\"name\":\"$NAME\",\"tag\":\"$TAG\"}}"
|
||||||
|
curl -X POST \
|
||||||
|
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$JSON" \
|
||||||
|
"https://${{ vars.RELEASE_URL }}/api/v1/repos/${OWNER}/Repository/actions/workflows/${WORKFLOW}/dispatches"
|
||||||
|
env:
|
||||||
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
OWNER: "AG_QGIS"
|
||||||
|
WORKFLOW: "release.yaml"
|
||||||
|
|
||||||
3
__pdoc__.py
Normal file
3
__pdoc__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
__pdoc__ = {
|
||||||
|
"main": False,
|
||||||
|
}
|
||||||
0
changelog.txt
Normal file
0
changelog.txt
Normal file
@@ -15,7 +15,7 @@ from .ly_metadata_wrapper import (
|
|||||||
is_layer_editable,
|
is_layer_editable,
|
||||||
)
|
)
|
||||||
from .ly_style_wrapper import apply_style
|
from .ly_style_wrapper import apply_style
|
||||||
from .dialog_wrapper import ask_yes_no
|
from .dialog_wrapper import ask_yes_no, ask_overwrite_append_cancel_custom
|
||||||
|
|
||||||
from .message_wrapper import (
|
from .message_wrapper import (
|
||||||
_get_message_bar,
|
_get_message_bar,
|
||||||
|
|||||||
@@ -1,62 +1,226 @@
|
|||||||
"""
|
"""
|
||||||
sn_basis/functions/dialog_wrapper.py – Benutzer-Dialoge
|
sn_basis/functions/dialog_wrapper.py – Benutzer-Dialoge (Qt5/6/Mock-kompatibel)
|
||||||
|
|
||||||
Dieser Wrapper kapselt alle Benutzer-Dialoge (z. B. Ja/Nein-Abfragen)
|
|
||||||
und sorgt dafür, dass sie sowohl in QGIS als auch im Mock-/Testmodus
|
|
||||||
einheitlich funktionieren.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from typing import Literal, Optional
|
||||||
# Import der abstrahierten Qt-Klassen aus dem qt_wrapper.
|
|
||||||
# QMessageBox, YES und NO sind bereits kompatibel zu Qt5/Qt6
|
|
||||||
# und im Mock-Modus durch Dummy-Objekte ersetzt.
|
|
||||||
from sn_basis.functions.qt_wrapper import (
|
from sn_basis.functions.qt_wrapper import (
|
||||||
QMessageBox,
|
QMessageBox, YES, NO, CANCEL, QT_VERSION, exec_dialog, ICON_QUESTION,
|
||||||
YES,
|
QProgressDialog, QCoreApplication, Qt, QInputDialog, QLineEdit,
|
||||||
NO,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Öffentliche API
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
def ask_yes_no(
|
def ask_yes_no(
|
||||||
title: str,
|
title: str,
|
||||||
message: str,
|
message: str,
|
||||||
default: bool = False,
|
default: bool = True,
|
||||||
parent: Any = None,
|
parent: Any = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Stellt dem Benutzer eine Ja/Nein-Frage.
|
Stellt Ja/Nein-Frage. Funktioniert in PyQt5/6 UND Mock-Modus.
|
||||||
|
|
||||||
- In einer echten QGIS-Umgebung wird ein QMessageBox-Dialog angezeigt.
|
|
||||||
- Im Mock-/Testmodus wird kein Dialog geöffnet, sondern der Default-Wert
|
|
||||||
zurückgegeben, damit Tests ohne UI laufen können.
|
|
||||||
|
|
||||||
:param title: Titel des Dialogs
|
|
||||||
:param message: Nachrichtentext
|
|
||||||
:param default: Rückgabewert im Fehler- oder Mock-Fall
|
|
||||||
:param parent: Optionales Parent-Widget
|
|
||||||
:return: True bei "Ja", False bei "Nein"
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Definiert die beiden Buttons, die angezeigt werden sollen.
|
if QT_VERSION == 0: # Mock-Modus
|
||||||
buttons = QMessageBox.Yes | QMessageBox.No
|
print(f"🔍 Mock-Modus: ask_yes_no('{title}') → {default}")
|
||||||
|
return default
|
||||||
|
|
||||||
|
# ✅ KORREKT: Verwende YES/NO-Aliase aus qt_wrapper!
|
||||||
|
buttons = YES | NO
|
||||||
|
default_button = YES if default else NO
|
||||||
|
|
||||||
# Öffnet den Dialog (oder im Mock-Modus: simuliert ihn).
|
|
||||||
result = QMessageBox.question(
|
result = QMessageBox.question(
|
||||||
parent,
|
parent, title, message, buttons, default_button
|
||||||
title,
|
|
||||||
message,
|
|
||||||
buttons,
|
|
||||||
YES if default else NO, # Vorauswahl abhängig vom Default
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Gibt True zurück, wenn der Benutzer "Ja" gewählt hat.
|
# ✅ int(result) == int(YES) funktioniert Qt5/6/Mock
|
||||||
return result == YES
|
print(f"DEBUG ask_yes_no: result={result}, YES={YES}, match={int(result) == int(YES)}")
|
||||||
|
return int(result) == int(YES)
|
||||||
|
|
||||||
except Exception:
|
except Exception as e:
|
||||||
# Falls Qt nicht verfügbar ist (Mock/CI), wird der Default-Wert genutzt.
|
print(f"⚠️ ask_yes_no Fehler: {e}")
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def show_info_dialog(title: str, message: str, parent: Any = None) -> None:
|
||||||
|
"""
|
||||||
|
Zeigt einen modalen Info-Dialog mit OK-Button.
|
||||||
|
Blockiert bis der Nutzer bestätigt.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if QT_VERSION == 0: # Mock-Modus
|
||||||
|
print(f"Mock-Modus: show_info_dialog('{title}')")
|
||||||
|
return
|
||||||
|
QMessageBox.information(parent, title, message)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ show_info_dialog Fehler: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def ask_text(
|
||||||
|
title: str,
|
||||||
|
label: str,
|
||||||
|
default_text: str = "",
|
||||||
|
parent: Any = None,
|
||||||
|
) -> tuple[str, bool]:
|
||||||
|
"""Zeigt einen modalen Texteingabe-Dialog und gibt Text + OK-Status zurück."""
|
||||||
|
try:
|
||||||
|
if QT_VERSION == 0: # Mock-Modus
|
||||||
|
print(f"Mock-Modus: ask_text('{title}') -> '{default_text}'")
|
||||||
|
return default_text, True
|
||||||
|
# PyQt6: QLineEdit.EchoMode.Normal / PyQt5: QLineEdit.Normal
|
||||||
|
echo_mode = (
|
||||||
|
getattr(QLineEdit, "Normal", None)
|
||||||
|
or getattr(getattr(QLineEdit, "EchoMode", None), "Normal", None)
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
text, accepted = QInputDialog.getText(
|
||||||
|
parent,
|
||||||
|
title,
|
||||||
|
label,
|
||||||
|
echo_mode,
|
||||||
|
default_text,
|
||||||
|
)
|
||||||
|
return str(text or ""), bool(accepted)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ ask_text Fehler: {e}")
|
||||||
|
return default_text, False
|
||||||
|
|
||||||
|
|
||||||
|
OverwriteDecision = Optional[Literal["overwrite", "append", "cancel"]]
|
||||||
|
|
||||||
|
|
||||||
|
def ask_overwrite_append_cancel_custom(
|
||||||
|
parent,
|
||||||
|
title: str,
|
||||||
|
message: str,
|
||||||
|
) -> Literal["overwrite", "append", "cancel"]:
|
||||||
|
"""Zeigt Dialog mit benutzerdefinierten Buttons: Überschreiben/Anhängen/Abbrechen.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
parent :
|
||||||
|
Eltern-Widget oder None.
|
||||||
|
title : str
|
||||||
|
Dialog-Titel.
|
||||||
|
message : str
|
||||||
|
Hauptmeldung mit Erklärung.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Literal["overwrite", "append", "cancel"]
|
||||||
|
Genaue Entscheidung des Nutzers.
|
||||||
|
"""
|
||||||
|
msg = QMessageBox(parent)
|
||||||
|
msg.setIcon(ICON_QUESTION)
|
||||||
|
msg.setWindowTitle(title)
|
||||||
|
msg.setText(message)
|
||||||
|
|
||||||
|
# Eigene Buttons mit exakten Texten
|
||||||
|
overwrite_btn = msg.addButton("Überschreiben", QMessageBox.ButtonRole.AcceptRole)
|
||||||
|
append_btn = msg.addButton("Anhängen", QMessageBox.ButtonRole.ActionRole)
|
||||||
|
cancel_btn = msg.addButton("Abbrechen", QMessageBox.ButtonRole.RejectRole)
|
||||||
|
|
||||||
|
exec_dialog(msg)
|
||||||
|
|
||||||
|
clicked = msg.clickedButton()
|
||||||
|
if clicked == overwrite_btn:
|
||||||
|
return "overwrite"
|
||||||
|
elif clicked == append_btn:
|
||||||
|
return "append"
|
||||||
|
else: # cancel_btn
|
||||||
|
return "cancel"
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressDialog:
|
||||||
|
def __init__(self, total: int, title: str = "Fortschritt", label: str = "Verarbeite..."):
|
||||||
|
self.total = max(total, 1)
|
||||||
|
self._canceled = False
|
||||||
|
|
||||||
|
if QT_VERSION == 0:
|
||||||
|
self.value = 0
|
||||||
|
self.label = label
|
||||||
|
self.title = title
|
||||||
|
return
|
||||||
|
|
||||||
|
self._dlg = QProgressDialog(label, "Abbrechen", 0, self.total)
|
||||||
|
self._dlg.setWindowTitle(title)
|
||||||
|
|
||||||
|
# Qt5 vs Qt6: WindowModality-Enum unterschiedlich verfügbar
|
||||||
|
modality = None
|
||||||
|
if hasattr(Qt, "WindowModality"):
|
||||||
|
try:
|
||||||
|
modality = Qt.WindowModality.WindowModal
|
||||||
|
except Exception:
|
||||||
|
modality = None
|
||||||
|
if modality is None and hasattr(Qt, "WindowModal"):
|
||||||
|
modality = Qt.WindowModal
|
||||||
|
if modality is not None:
|
||||||
|
try:
|
||||||
|
self._dlg.setWindowModality(modality)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._dlg.setMinimumDuration(0)
|
||||||
|
self._dlg.setAutoClose(False)
|
||||||
|
self._dlg.setAutoReset(False)
|
||||||
|
self._dlg.setValue(0)
|
||||||
|
|
||||||
|
def on_cancel():
|
||||||
|
if self._dlg and self._dlg.value() >= self.total:
|
||||||
|
# OK-Button am Ende
|
||||||
|
self._dlg.close()
|
||||||
|
return
|
||||||
|
self._canceled = True
|
||||||
|
self._dlg.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._dlg.canceled.connect(on_cancel)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_total(self, total: int) -> None:
|
||||||
|
self.total = max(total, 1)
|
||||||
|
if QT_VERSION == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._dlg is not None:
|
||||||
|
self._dlg.setMaximum(self.total)
|
||||||
|
|
||||||
|
def set_value(self, value: int) -> None:
|
||||||
|
if QT_VERSION == 0:
|
||||||
|
self.value = value
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._dlg is not None:
|
||||||
|
self._dlg.setValue(min(value, self.total))
|
||||||
|
if value >= self.total:
|
||||||
|
self._dlg.setLabelText("Fertig. Klicken Sie auf OK, um das Fenster zu schließen.")
|
||||||
|
self._dlg.setCancelButtonText("OK")
|
||||||
|
QCoreApplication.processEvents()
|
||||||
|
|
||||||
|
def set_label(self, text: str) -> None:
|
||||||
|
if QT_VERSION == 0:
|
||||||
|
self.label = text
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._dlg is not None:
|
||||||
|
self._dlg.setLabelText(text)
|
||||||
|
QCoreApplication.processEvents()
|
||||||
|
|
||||||
|
def is_canceled(self) -> bool:
|
||||||
|
if QT_VERSION == 0:
|
||||||
|
return self._canceled
|
||||||
|
|
||||||
|
if self._dlg is not None:
|
||||||
|
return self._canceled or self._dlg.wasCanceled()
|
||||||
|
|
||||||
|
return self._canceled
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
if QT_VERSION == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._dlg is not None:
|
||||||
|
self._dlg.close()
|
||||||
|
|
||||||
|
|
||||||
|
def create_progress_dialog(total: int, title: str = "Fortschritt", label: str = "Verarbeite...") -> ProgressDialog:
|
||||||
|
return ProgressDialog(total, title, label)
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,44 @@
|
|||||||
# sn_basis/functions/ly_style_wrapper.py
|
# sn_basis/functions/ly_style_wrapper.py
|
||||||
|
|
||||||
from sn_basis.functions.ly_existence_wrapper import layer_exists
|
from sn_basis.functions.ly_existence_wrapper import layer_exists
|
||||||
from sn_basis.functions.sys_wrapper import (
|
from sn_basis.functions.sys_wrapper import get_plugin_root, join_path
|
||||||
get_plugin_root,
|
from sn_basis.modules.stilpruefer import Stilpruefer
|
||||||
join_path,
|
from typing import Optional
|
||||||
file_exists,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def apply_style(layer, style_name: str) -> bool:
|
def apply_style(layer, style_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Wendet einen Layerstil an, sofern er gültig ist.
|
||||||
|
|
||||||
|
- Validierung erfolgt ausschließlich über Stilpruefer
|
||||||
|
- Keine eigenen Dateisystem- oder Endungsprüfungen
|
||||||
|
- Keine Seiteneffekte bei ungültigem Stil
|
||||||
|
"""
|
||||||
|
print(">>> apply_style() START")
|
||||||
|
|
||||||
if not layer_exists(layer):
|
if not layer_exists(layer):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
style_path = join_path(get_plugin_root(), "styles", style_name)
|
# Stilpfad zusammensetzen
|
||||||
if not file_exists(style_path):
|
style_path = join_path(get_plugin_root(), "sn_verfahrensgebiet","styles", style_name)
|
||||||
|
|
||||||
|
# Stil prüfen
|
||||||
|
pruefer = Stilpruefer()
|
||||||
|
ergebnis = pruefer.pruefe(style_path)
|
||||||
|
print(">>> Stilprüfung:", ergebnis)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[Stilprüfung] ok={ergebnis.ok} | "
|
||||||
|
f"aktion={ergebnis.aktion} | "
|
||||||
|
f"meldung={ergebnis.meldung}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if not ergebnis.ok:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Stil anwenden
|
||||||
try:
|
try:
|
||||||
ok, _ = layer.loadNamedStyle(style_path)
|
ok, _ = layer.loadNamedStyle(str(ergebnis.kontext))
|
||||||
if ok:
|
if ok:
|
||||||
getattr(layer, "triggerRepaint", lambda: None)()
|
getattr(layer, "triggerRepaint", lambda: None)()
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -57,6 +57,22 @@ def get_home_dir() -> Path:
|
|||||||
return Path.home()
|
return Path.home()
|
||||||
|
|
||||||
|
|
||||||
|
def is_absolute_path(path: _PathLike) -> bool:
|
||||||
|
"""Prüft, ob ein Pfad absolut ist."""
|
||||||
|
try:
|
||||||
|
return Path(path).is_absolute()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def basename(path: _PathLike) -> str:
|
||||||
|
"""Gibt den finalen Namen des Pfades zurück (Dateiname oder Ordner)."""
|
||||||
|
try:
|
||||||
|
return Path(path).name
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Dateisystem-Eigenschaften
|
# Dateisystem-Eigenschaften
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -75,3 +91,11 @@ def is_case_sensitive_fs() -> bool:
|
|||||||
|
|
||||||
# Linux praktisch immer case-sensitiv
|
# Linux praktisch immer case-sensitiv
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def path_suffix(path: _PathLike) -> str:
|
||||||
|
"""Gibt die Dateiendung eines Pfades zurück (inklusive Punkt)."""
|
||||||
|
try:
|
||||||
|
return Path(path).suffix
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|||||||
@@ -20,6 +20,19 @@ QgsNetworkAccessManager: Type[Any]
|
|||||||
Qgis: Type[Any]
|
Qgis: Type[Any]
|
||||||
QgsMapLayerProxyModel: Type[Any]
|
QgsMapLayerProxyModel: Type[Any]
|
||||||
QgsVectorFileWriter: Type[Any] # neu: Schreib-API
|
QgsVectorFileWriter: Type[Any] # neu: Schreib-API
|
||||||
|
QgsFeature: Type[Any]
|
||||||
|
QgsField: Type[Any]
|
||||||
|
QgsGeometry: Type[Any]
|
||||||
|
QgsFeatureRequest: Type[Any]
|
||||||
|
QgsCoordinateTransform: Type[Any]
|
||||||
|
QgsCoordinateReferenceSystem: Type[Any]
|
||||||
|
QgsPrintLayout: Type[Any]
|
||||||
|
QgsLayoutItemMap: Type[Any]
|
||||||
|
QgsLayoutItemLabel: Type[Any]
|
||||||
|
QgsLayoutPoint: Type[Any]
|
||||||
|
QgsLayoutSize: Type[Any]
|
||||||
|
QgsUnitTypes: Type[Any]
|
||||||
|
QgsLayoutItem: Type[Any]
|
||||||
|
|
||||||
QGIS_AVAILABLE = False
|
QGIS_AVAILABLE = False
|
||||||
|
|
||||||
@@ -36,6 +49,19 @@ try:
|
|||||||
Qgis as _Qgis,
|
Qgis as _Qgis,
|
||||||
QgsMapLayerProxyModel as _QgsMaplLayerProxyModel,
|
QgsMapLayerProxyModel as _QgsMaplLayerProxyModel,
|
||||||
QgsVectorFileWriter as _QgsVectorFileWriter,
|
QgsVectorFileWriter as _QgsVectorFileWriter,
|
||||||
|
QgsFeature as _QgsFeature,
|
||||||
|
QgsField as _QgsField,
|
||||||
|
QgsGeometry as _QgsGeometry,
|
||||||
|
QgsFeatureRequest as _QgsFeatureRequest,
|
||||||
|
QgsCoordinateTransform as _QgsCoordinateTransform,
|
||||||
|
QgsCoordinateReferenceSystem as _QgsCoordinateReferenceSystem,
|
||||||
|
QgsPrintLayout as _QgsPrintLayout,
|
||||||
|
QgsLayoutItemMap as _QgsLayoutItemMap,
|
||||||
|
QgsLayoutItemLabel as _QgsLayoutItemLabel,
|
||||||
|
QgsLayoutPoint as _QgsLayoutPoint,
|
||||||
|
QgsLayoutSize as _QgsLayoutSize,
|
||||||
|
QgsUnitTypes as _QgsUnitTypes,
|
||||||
|
QgsLayoutItem as _QgsLayoutItem,
|
||||||
)
|
)
|
||||||
|
|
||||||
QgsProject = _QgsProject
|
QgsProject = _QgsProject
|
||||||
@@ -45,6 +71,19 @@ try:
|
|||||||
Qgis = _Qgis
|
Qgis = _Qgis
|
||||||
QgsMapLayerProxyModel = _QgsMaplLayerProxyModel
|
QgsMapLayerProxyModel = _QgsMaplLayerProxyModel
|
||||||
QgsVectorFileWriter = _QgsVectorFileWriter
|
QgsVectorFileWriter = _QgsVectorFileWriter
|
||||||
|
QgsFeature = _QgsFeature
|
||||||
|
QgsField = _QgsField
|
||||||
|
QgsGeometry = _QgsGeometry
|
||||||
|
QgsFeatureRequest = _QgsFeatureRequest
|
||||||
|
QgsCoordinateTransform = _QgsCoordinateTransform
|
||||||
|
QgsCoordinateReferenceSystem = _QgsCoordinateReferenceSystem
|
||||||
|
QgsPrintLayout = _QgsPrintLayout
|
||||||
|
QgsLayoutItemMap = _QgsLayoutItemMap
|
||||||
|
QgsLayoutItemLabel = _QgsLayoutItemLabel
|
||||||
|
QgsLayoutPoint = _QgsLayoutPoint
|
||||||
|
QgsLayoutSize = _QgsLayoutSize
|
||||||
|
QgsUnitTypes = _QgsUnitTypes
|
||||||
|
QgsLayoutItem = _QgsLayoutItem
|
||||||
|
|
||||||
QGIS_AVAILABLE = True
|
QGIS_AVAILABLE = True
|
||||||
|
|
||||||
@@ -55,9 +94,17 @@ try:
|
|||||||
except Exception:
|
except Exception:
|
||||||
QGIS_AVAILABLE = False
|
QGIS_AVAILABLE = False
|
||||||
|
|
||||||
|
class _MockLayoutManager:
|
||||||
|
def layoutByName(self, name: str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def addLayout(self, layout: Any) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
class _MockQgsProject:
|
class _MockQgsProject:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._variables = {}
|
self._variables = {}
|
||||||
|
self._layout_manager = _MockLayoutManager()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def instance() -> "_MockQgsProject":
|
def instance() -> "_MockQgsProject":
|
||||||
@@ -66,6 +113,9 @@ except Exception:
|
|||||||
def read(self) -> bool:
|
def read(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def layoutManager(self):
|
||||||
|
return self._layout_manager
|
||||||
|
|
||||||
QgsProject = _MockQgsProject
|
QgsProject = _MockQgsProject
|
||||||
|
|
||||||
class _MockQgsVectorLayer:
|
class _MockQgsVectorLayer:
|
||||||
@@ -116,6 +166,134 @@ except Exception:
|
|||||||
|
|
||||||
QgsRasterLayer = _MockQgsRasterLayer
|
QgsRasterLayer = _MockQgsRasterLayer
|
||||||
|
|
||||||
|
class _MockQgsPrintLayout:
|
||||||
|
def __init__(self, project: Any):
|
||||||
|
self.project = project
|
||||||
|
self._name = ""
|
||||||
|
self._page = _MockQgsLayoutPage()
|
||||||
|
|
||||||
|
def initializeDefaults(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setName(self, name: str) -> None:
|
||||||
|
self._name = name
|
||||||
|
|
||||||
|
def pageCollection(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def page(self, index: int):
|
||||||
|
return self._page
|
||||||
|
|
||||||
|
def addLayoutItem(self, item: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _MockQgsLayoutPage:
|
||||||
|
def setPageSize(self, size: Any) -> None:
|
||||||
|
self.size = size
|
||||||
|
|
||||||
|
class _MockQgsLayoutItem:
|
||||||
|
class ReferencePoint:
|
||||||
|
LowerLeft = 0
|
||||||
|
|
||||||
|
class _MockQgsLayoutItemMap:
|
||||||
|
def __init__(self, layout: Any):
|
||||||
|
self.layout = layout
|
||||||
|
|
||||||
|
def setId(self, item_id: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setExtent(self, extent: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setScale(self, scale: float) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def attemptMove(self, point: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def attemptResize(self, size: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setFollowVisibilityPreset(self, active: bool) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setFollowVisibilityPresetName(self, name: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _MockQgsLayoutItemLabel:
|
||||||
|
ModeHtml = 1
|
||||||
|
|
||||||
|
def __init__(self, layout: Any):
|
||||||
|
self.layout = layout
|
||||||
|
|
||||||
|
def setId(self, item_id: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setText(self, text: str) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setMode(self, mode: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setFont(self, font: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setReferencePoint(self, point: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def attemptMove(self, point: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def attemptResize(self, size: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _MockQgsLayoutPoint:
|
||||||
|
def __init__(self, x: float, y: float, unit: Any):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.unit = unit
|
||||||
|
|
||||||
|
class _MockQgsLayoutSize:
|
||||||
|
def __init__(self, width: float, height: float, unit: Any):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.unit = unit
|
||||||
|
|
||||||
|
class _MockQgsUnitTypes:
|
||||||
|
LayoutMillimeters = 0
|
||||||
|
|
||||||
|
QgsPrintLayout = _MockQgsPrintLayout
|
||||||
|
QgsLayoutItemMap = _MockQgsLayoutItemMap
|
||||||
|
QgsLayoutItemLabel = _MockQgsLayoutItemLabel
|
||||||
|
QgsLayoutPoint = _MockQgsLayoutPoint
|
||||||
|
QgsLayoutSize = _MockQgsLayoutSize
|
||||||
|
QgsUnitTypes = _MockQgsUnitTypes
|
||||||
|
QgsLayoutItem = _MockQgsLayoutItem
|
||||||
|
|
||||||
|
class _MockQgsFeatureRequest:
|
||||||
|
def __init__(self):
|
||||||
|
self._filter_rect = None
|
||||||
|
|
||||||
|
def setFilterRect(self, rect):
|
||||||
|
self._filter_rect = rect
|
||||||
|
return self
|
||||||
|
|
||||||
|
QgsFeatureRequest = _MockQgsFeatureRequest
|
||||||
|
|
||||||
|
class _MockQgsCoordinateTransform:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def transformBoundingBox(self, rect):
|
||||||
|
return rect
|
||||||
|
|
||||||
|
class _MockQgsCoordinateReferenceSystem:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
QgsCoordinateTransform = _MockQgsCoordinateTransform
|
||||||
|
QgsCoordinateReferenceSystem = _MockQgsCoordinateReferenceSystem
|
||||||
|
|
||||||
QgsNetworkAccessManager = _MockQgsNetworkAccessManager
|
QgsNetworkAccessManager = _MockQgsNetworkAccessManager
|
||||||
|
|
||||||
class _MockQgis:
|
class _MockQgis:
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ except Exception:
|
|||||||
def removeToolBar(self, *args, **kwargs):
|
def removeToolBar(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def openLayoutDesigner(self, layout):
|
||||||
|
return layout
|
||||||
|
|
||||||
iface = _MockIface()
|
iface = _MockIface()
|
||||||
|
|
||||||
class _MockQgsFileWidget:
|
class _MockQgsFileWidget:
|
||||||
@@ -132,6 +135,13 @@ def get_main_window():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def open_layout_designer(layout: Any) -> Any:
|
||||||
|
try:
|
||||||
|
return iface.openLayoutDesigner(layout)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# Dock-Handling
|
# Dock-Handling
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|||||||
@@ -1,163 +1,82 @@
|
|||||||
"""
|
"""
|
||||||
sn_basis/functions/qt_wrapper.py – zentrale Qt-Abstraktion (PyQt5 / PyQt6 / Mock)
|
sn_basis/functions/qt_wrapper.py – zentrale Qt-Abstraktion (PyQt6 primär / PyQt5 Fallback / Mock)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional, Type, Any
|
from typing import Optional, Type, Any, Callable
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Qt-Symbole (werden dynamisch gesetzt)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
QDockWidget: Type[Any]
|
|
||||||
QMessageBox: Type[Any]
|
|
||||||
QFileDialog: Type[Any]
|
|
||||||
QEventLoop: Type[Any]
|
|
||||||
QUrl: Type[Any]
|
|
||||||
QNetworkRequest: Type[Any]
|
|
||||||
QNetworkReply: Type[Any]
|
|
||||||
QCoreApplication: Type[Any]
|
|
||||||
|
|
||||||
QWidget: Type[Any]
|
|
||||||
QGridLayout: Type[Any]
|
|
||||||
QLabel: Type[Any]
|
|
||||||
QLineEdit: Type[Any]
|
|
||||||
QGroupBox: Type[Any]
|
|
||||||
QVBoxLayout: Type[Any]
|
|
||||||
QPushButton: Type[Any]
|
|
||||||
QAction: Type[Any]
|
|
||||||
QMenu: Type[Any]
|
|
||||||
QToolBar: Type[Any]
|
|
||||||
QActionGroup: Type[Any]
|
|
||||||
QTabWidget: type
|
|
||||||
QToolButton: Type[Any]
|
|
||||||
QSizePolicy: Type[Any]
|
|
||||||
Qt: Type[Any]
|
|
||||||
QComboBox: Type[Any]
|
|
||||||
|
|
||||||
|
# Globale Qt-Symbole (werden dynamisch gesetzt)
|
||||||
|
QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6
|
||||||
YES: Optional[Any] = None
|
YES: Optional[Any] = None
|
||||||
NO: Optional[Any] = None
|
NO: Optional[Any] = None
|
||||||
CANCEL: Optional[Any] = None
|
CANCEL: Optional[Any] = None
|
||||||
ICON_QUESTION: Optional[Any] = None
|
ICON_QUESTION: Optional[Any] = None
|
||||||
|
QVariant: Type[Any] = object
|
||||||
|
|
||||||
QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6
|
|
||||||
|
|
||||||
|
# Qt-Klassen (werden dynamisch gesetzt)
|
||||||
|
QDockWidget: Type[Any] = object
|
||||||
|
QMessageBox: Type[Any] = object
|
||||||
|
QFileDialog: Type[Any] = object
|
||||||
|
QProgressDialog: Type[Any] = object
|
||||||
|
QEventLoop: Type[Any] = object
|
||||||
|
QTimer: Type[Any] = object
|
||||||
|
QUrl: Type[Any] = object
|
||||||
|
QNetworkRequest: Type[Any] = object
|
||||||
|
QNetworkReply: Type[Any] = object
|
||||||
|
QCoreApplication: Type[Any] = object
|
||||||
|
QWidget: Type[Any] = object
|
||||||
|
QGridLayout: Type[Any] = object
|
||||||
|
QLabel: Type[Any] = object
|
||||||
|
QLineEdit: Type[Any] = object
|
||||||
|
QInputDialog: Type[Any] = object
|
||||||
|
QGroupBox: Type[Any] = object
|
||||||
|
QVBoxLayout: Type[Any] = object
|
||||||
|
QPushButton: Type[Any] = object
|
||||||
|
QAction: Type[Any] = object
|
||||||
|
QMenu: Type[Any] = object
|
||||||
|
QToolBar: Type[Any] = object
|
||||||
|
QActionGroup: Type[Any] = object
|
||||||
|
QTabWidget: Type[Any] = object
|
||||||
|
QToolButton: Type[Any] = object
|
||||||
|
QSizePolicy: Type[Any] = object
|
||||||
|
Qt: Type[Any] = object
|
||||||
|
QComboBox: Type[Any] = object
|
||||||
|
QCheckBox: Type[Any] = object
|
||||||
|
QHBoxLayout: Type[Any] = object
|
||||||
|
QFont: Type[Any] = object
|
||||||
|
|
||||||
|
|
||||||
def exec_dialog(dialog: Any) -> Any:
|
def exec_dialog(dialog: Any) -> Any:
|
||||||
raise NotImplementedError
|
"""Führt Dialog modal aus (Qt6: exec(), Qt5: exec_(), Mock: YES)"""
|
||||||
|
raise NotImplementedError("Qt nicht initialisiert")
|
||||||
|
|
||||||
|
def debug_qt_status() -> None:
|
||||||
|
"""Debug: Zeigt Qt-Status für Troubleshooting."""
|
||||||
|
print(f"🔍 QT_VERSION: {QT_VERSION}")
|
||||||
|
print(f"🔍 QMessageBox Typ: {getattr(QMessageBox, '__name__', type(QMessageBox).__name__)}")
|
||||||
|
print(f"🔍 YES Wert: {YES} (Typ: {type(YES) if YES is not None else 'None'})")
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
if QT_VERSION == 0:
|
||||||
# Versuch: PyQt6
|
print("❌ MOCK-MODUS AKTIV! Keine Dialoge möglich!")
|
||||||
# ---------------------------------------------------------
|
elif QT_VERSION == 5:
|
||||||
|
print("✅ PyQt5 geladen (Fallback) – Dialoge sollten funktionieren!")
|
||||||
|
elif QT_VERSION == 6:
|
||||||
|
print("✅ PyQt6 geladen (primär) – Dialoge sollten funktionieren!")
|
||||||
|
else:
|
||||||
|
print("❓ Unbekannte Qt-Version!")
|
||||||
|
|
||||||
|
# --------------------------- PYQT6 PRIMÄR ---------------------------
|
||||||
try:
|
try:
|
||||||
from qgis.PyQt.QtWidgets import ( # type: ignore
|
from qgis.PyQt.QtWidgets import (
|
||||||
QMessageBox as _QMessageBox,# type: ignore
|
|
||||||
QFileDialog as _QFileDialog,# type: ignore
|
|
||||||
QWidget as _QWidget,# type: ignore
|
|
||||||
QGridLayout as _QGridLayout,# type: ignore
|
|
||||||
QLabel as _QLabel,# type: ignore
|
|
||||||
QLineEdit as _QLineEdit,# type: ignore
|
|
||||||
QGroupBox as _QGroupBox,# type: ignore
|
|
||||||
QVBoxLayout as _QVBoxLayout,# type: ignore
|
|
||||||
QPushButton as _QPushButton,# type: ignore
|
|
||||||
QAction as _QAction,
|
|
||||||
QMenu as _QMenu,# type: ignore
|
|
||||||
QToolBar as _QToolBar,# type: ignore
|
|
||||||
QActionGroup as _QActionGroup,# type: ignore
|
|
||||||
QDockWidget as _QDockWidget,# type: ignore
|
|
||||||
QTabWidget as _QTabWidget,# type: ignore
|
|
||||||
QToolButton as _QToolButton,#type:ignore
|
|
||||||
QSizePolicy as _QSizePolicy,#type:ignore
|
|
||||||
QComboBox as _QComboBox,
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from qgis.PyQt.QtCore import ( # type: ignore
|
|
||||||
QEventLoop as _QEventLoop,# type: ignore
|
|
||||||
QUrl as _QUrl,# type: ignore
|
|
||||||
QCoreApplication as _QCoreApplication,# type: ignore
|
|
||||||
Qt as _Qt#type:ignore
|
|
||||||
)
|
|
||||||
from qgis.PyQt.QtNetwork import ( # type: ignore
|
|
||||||
QNetworkRequest as _QNetworkRequest,# type: ignore
|
|
||||||
QNetworkReply as _QNetworkReply,# type: ignore
|
|
||||||
)
|
|
||||||
QT_VERSION = 6
|
|
||||||
QMessageBox = _QMessageBox
|
|
||||||
QFileDialog = _QFileDialog
|
|
||||||
QEventLoop = _QEventLoop
|
|
||||||
QUrl = _QUrl
|
|
||||||
QNetworkRequest = _QNetworkRequest
|
|
||||||
QNetworkReply = _QNetworkReply
|
|
||||||
QCoreApplication = _QCoreApplication
|
|
||||||
Qt=_Qt
|
|
||||||
QDockWidget = _QDockWidget
|
|
||||||
QWidget = _QWidget
|
|
||||||
QGridLayout = _QGridLayout
|
|
||||||
QLabel = _QLabel
|
|
||||||
QLineEdit = _QLineEdit
|
|
||||||
QGroupBox = _QGroupBox
|
|
||||||
QVBoxLayout = _QVBoxLayout
|
|
||||||
QPushButton = _QPushButton
|
|
||||||
QAction = _QAction
|
|
||||||
QMenu = _QMenu
|
|
||||||
QToolBar = _QToolBar
|
|
||||||
QActionGroup = _QActionGroup
|
|
||||||
QTabWidget = _QTabWidget
|
|
||||||
QToolButton=_QToolButton
|
|
||||||
QSizePolicy=_QSizePolicy
|
|
||||||
QComboBox=_QComboBox
|
|
||||||
|
|
||||||
YES = QMessageBox.StandardButton.Yes
|
|
||||||
NO = QMessageBox.StandardButton.No
|
|
||||||
CANCEL = QMessageBox.StandardButton.Cancel
|
|
||||||
ICON_QUESTION = QMessageBox.Icon.Question
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Qt6 Enum-Aliase (vereinheitlicht)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
ToolButtonTextBesideIcon = Qt.ToolButtonStyle.ToolButtonTextBesideIcon
|
|
||||||
ArrowDown = Qt.ArrowType.DownArrow
|
|
||||||
ArrowRight = Qt.ArrowType.RightArrow
|
|
||||||
# QSizePolicy Enum-Aliase (Qt6)
|
|
||||||
SizePolicyPreferred = QSizePolicy.Policy.Preferred
|
|
||||||
SizePolicyMaximum = QSizePolicy.Policy.Maximum
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# QDockWidget Feature-Aliase (Qt6)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
DockWidgetMovable = QDockWidget.DockWidgetFeature.DockWidgetMovable
|
|
||||||
DockWidgetFloatable = QDockWidget.DockWidgetFeature.DockWidgetFloatable
|
|
||||||
DockWidgetClosable = QDockWidget.DockWidgetFeature.DockWidgetClosable
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Dock-Area-Aliase (Qt6)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
DockAreaLeft = Qt.DockWidgetArea.LeftDockWidgetArea
|
|
||||||
DockAreaRight = Qt.DockWidgetArea.RightDockWidgetArea
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def exec_dialog(dialog: Any) -> Any:
|
|
||||||
return dialog.exec()
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Versuch: PyQt5
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
from PyQt5.QtWidgets import (# type: ignore
|
|
||||||
QMessageBox as _QMessageBox,
|
QMessageBox as _QMessageBox,
|
||||||
QFileDialog as _QFileDialog,
|
QFileDialog as _QFileDialog,
|
||||||
|
QProgressDialog as _QProgressDialog,
|
||||||
QWidget as _QWidget,
|
QWidget as _QWidget,
|
||||||
QGridLayout as _QGridLayout,
|
QGridLayout as _QGridLayout,
|
||||||
QLabel as _QLabel,
|
QLabel as _QLabel,
|
||||||
QLineEdit as _QLineEdit,
|
QLineEdit as _QLineEdit,
|
||||||
|
QInputDialog as _QInputDialog,
|
||||||
QGroupBox as _QGroupBox,
|
QGroupBox as _QGroupBox,
|
||||||
QVBoxLayout as _QVBoxLayout,
|
QVBoxLayout as _QVBoxLayout,
|
||||||
QPushButton as _QPushButton,
|
QPushButton as _QPushButton,
|
||||||
@@ -170,34 +89,42 @@ except Exception:
|
|||||||
QToolButton as _QToolButton,
|
QToolButton as _QToolButton,
|
||||||
QSizePolicy as _QSizePolicy,
|
QSizePolicy as _QSizePolicy,
|
||||||
QComboBox as _QComboBox,
|
QComboBox as _QComboBox,
|
||||||
|
QCheckBox as _QCheckBox,
|
||||||
|
QHBoxLayout as _QHBoxLayout,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import (# type: ignore
|
from qgis.PyQt.QtGui import QFont as _QFont
|
||||||
|
from qgis.PyQt.QtCore import (
|
||||||
QEventLoop as _QEventLoop,
|
QEventLoop as _QEventLoop,
|
||||||
|
QTimer as _QTimer,
|
||||||
QUrl as _QUrl,
|
QUrl as _QUrl,
|
||||||
QCoreApplication as _QCoreApplication,
|
QCoreApplication as _QCoreApplication,
|
||||||
Qt as _Qt,
|
Qt as _Qt,
|
||||||
|
QVariant as _QVariant
|
||||||
)
|
)
|
||||||
from PyQt5.QtNetwork import (# type: ignore
|
from qgis.PyQt.QtNetwork import (
|
||||||
QNetworkRequest as _QNetworkRequest,
|
QNetworkRequest as _QNetworkRequest,
|
||||||
QNetworkReply as _QNetworkReply,
|
QNetworkReply as _QNetworkReply,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ✅ ALLE GLOBALS ZUWEISEN
|
||||||
|
QT_VERSION = 6
|
||||||
QMessageBox = _QMessageBox
|
QMessageBox = _QMessageBox
|
||||||
QFileDialog = _QFileDialog
|
QFileDialog = _QFileDialog
|
||||||
|
QProgressDialog = _QProgressDialog
|
||||||
|
QProgressDialog = _QProgressDialog
|
||||||
QEventLoop = _QEventLoop
|
QEventLoop = _QEventLoop
|
||||||
|
QTimer = _QTimer
|
||||||
QUrl = _QUrl
|
QUrl = _QUrl
|
||||||
QNetworkRequest = _QNetworkRequest
|
QNetworkRequest = _QNetworkRequest
|
||||||
QNetworkReply = _QNetworkReply
|
QNetworkReply = _QNetworkReply
|
||||||
QCoreApplication = _QCoreApplication
|
QCoreApplication = _QCoreApplication
|
||||||
Qt = _Qt
|
Qt = _Qt
|
||||||
QDockWidget = _QDockWidget
|
QDockWidget = _QDockWidget
|
||||||
|
|
||||||
|
|
||||||
QWidget = _QWidget
|
QWidget = _QWidget
|
||||||
QGridLayout = _QGridLayout
|
QGridLayout = _QGridLayout
|
||||||
QLabel = _QLabel
|
QLabel = _QLabel
|
||||||
QLineEdit = _QLineEdit
|
QLineEdit = _QLineEdit
|
||||||
|
QInputDialog = _QInputDialog
|
||||||
QGroupBox = _QGroupBox
|
QGroupBox = _QGroupBox
|
||||||
QVBoxLayout = _QVBoxLayout
|
QVBoxLayout = _QVBoxLayout
|
||||||
QPushButton = _QPushButton
|
QPushButton = _QPushButton
|
||||||
@@ -208,53 +135,145 @@ except Exception:
|
|||||||
QTabWidget = _QTabWidget
|
QTabWidget = _QTabWidget
|
||||||
QToolButton = _QToolButton
|
QToolButton = _QToolButton
|
||||||
QSizePolicy = _QSizePolicy
|
QSizePolicy = _QSizePolicy
|
||||||
ComboBox=_QComboBox
|
QComboBox = _QComboBox
|
||||||
|
QCheckBox = _QCheckBox
|
||||||
|
QVariant = _QVariant
|
||||||
|
QHBoxLayout = _QHBoxLayout
|
||||||
|
QFont = _QFont
|
||||||
|
# ✅ QT6 ENUMS
|
||||||
|
YES = QMessageBox.StandardButton.Yes
|
||||||
|
NO = QMessageBox.StandardButton.No
|
||||||
|
CANCEL = QMessageBox.StandardButton.Cancel
|
||||||
|
ICON_QUESTION = QMessageBox.Icon.Question
|
||||||
|
AcceptRole = QMessageBox.ButtonRole.AcceptRole
|
||||||
|
ActionRole = QMessageBox.ButtonRole.ActionRole
|
||||||
|
RejectRole = QMessageBox.ButtonRole.RejectRole
|
||||||
|
|
||||||
|
# Qt6 Enum-Aliase
|
||||||
|
ToolButtonTextBesideIcon = Qt.ToolButtonStyle.ToolButtonTextBesideIcon
|
||||||
|
ArrowDown = Qt.ArrowType.DownArrow
|
||||||
|
ArrowRight = Qt.ArrowType.RightArrow
|
||||||
|
SizePolicyPreferred = QSizePolicy.Policy.Preferred
|
||||||
|
SizePolicyMaximum = QSizePolicy.Policy.Maximum
|
||||||
|
DockWidgetMovable = QDockWidget.DockWidgetFeature.DockWidgetMovable
|
||||||
|
DockWidgetFloatable = QDockWidget.DockWidgetFeature.DockWidgetFloatable
|
||||||
|
DockWidgetClosable = QDockWidget.DockWidgetFeature.DockWidgetClosable
|
||||||
|
DockAreaLeft = Qt.DockWidgetArea.LeftDockWidgetArea
|
||||||
|
DockAreaRight = Qt.DockWidgetArea.RightDockWidgetArea
|
||||||
|
|
||||||
|
def exec_dialog(dialog: Any) -> Any:
|
||||||
|
return dialog.exec()
|
||||||
|
|
||||||
|
print(f"✅ qt_wrapper: PyQt6 geladen (QT_VERSION={QT_VERSION})")
|
||||||
|
|
||||||
|
# --------------------------- PYQT5 FALLBACK ---------------------------
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
try:
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QMessageBox as _QMessageBox,
|
||||||
|
QFileDialog as _QFileDialog,
|
||||||
|
QWidget as _QWidget,
|
||||||
|
QGridLayout as _QGridLayout,
|
||||||
|
QLabel as _QLabel,
|
||||||
|
QLineEdit as _QLineEdit,
|
||||||
|
QInputDialog as _QInputDialog,
|
||||||
|
QGroupBox as _QGroupBox,
|
||||||
|
QVBoxLayout as _QVBoxLayout,
|
||||||
|
QPushButton as _QPushButton,
|
||||||
|
QAction as _QAction,
|
||||||
|
QMenu as _QMenu,
|
||||||
|
QToolBar as _QToolBar,
|
||||||
|
QActionGroup as _QActionGroup,
|
||||||
|
QDockWidget as _QDockWidget,
|
||||||
|
QTabWidget as _QTabWidget,
|
||||||
|
QToolButton as _QToolButton,
|
||||||
|
QSizePolicy as _QSizePolicy,
|
||||||
|
QComboBox as _QComboBox,
|
||||||
|
QCheckBox as _QCheckBox,
|
||||||
|
QHBoxLayout as _QHBoxLayout,
|
||||||
|
)
|
||||||
|
from PyQt5.QtGui import QFont as _QFont
|
||||||
|
from PyQt5.QtCore import (
|
||||||
|
QEventLoop as _QEventLoop,
|
||||||
|
QTimer as _QTimer,
|
||||||
|
QUrl as _QUrl,
|
||||||
|
QCoreApplication as _QCoreApplication,
|
||||||
|
Qt as _Qt,
|
||||||
|
QVariant as _QVariant
|
||||||
|
)
|
||||||
|
from PyQt5.QtNetwork import (
|
||||||
|
QNetworkRequest as _QNetworkRequest,
|
||||||
|
QNetworkReply as _QNetworkReply,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ✅ ALLE GLOBALS ZUWEISEN
|
||||||
|
QT_VERSION = 5
|
||||||
|
QMessageBox = _QMessageBox
|
||||||
|
QFileDialog = _QFileDialog
|
||||||
|
QEventLoop = _QEventLoop
|
||||||
|
QTimer = _QTimer
|
||||||
|
QUrl = _QUrl
|
||||||
|
QNetworkRequest = _QNetworkRequest
|
||||||
|
QNetworkReply = _QNetworkReply
|
||||||
|
QCoreApplication = _QCoreApplication
|
||||||
|
Qt = _Qt
|
||||||
|
QDockWidget = _QDockWidget
|
||||||
|
QWidget = _QWidget
|
||||||
|
QGridLayout = _QGridLayout
|
||||||
|
QLabel = _QLabel
|
||||||
|
QLineEdit = _QLineEdit
|
||||||
|
QInputDialog = _QInputDialog
|
||||||
|
QGroupBox = _QGroupBox
|
||||||
|
QVBoxLayout = _QVBoxLayout
|
||||||
|
QPushButton = _QPushButton
|
||||||
|
QAction = _QAction
|
||||||
|
QMenu = _QMenu
|
||||||
|
QToolBar = _QToolBar
|
||||||
|
QActionGroup = _QActionGroup
|
||||||
|
QTabWidget = _QTabWidget
|
||||||
|
QToolButton = _QToolButton
|
||||||
|
QSizePolicy = _QSizePolicy
|
||||||
|
QComboBox = _QComboBox
|
||||||
|
QCheckBox = _QCheckBox
|
||||||
|
QVariant = _QVariant
|
||||||
|
QHBoxLayout= _QHBoxLayout
|
||||||
|
QFont = _QFont
|
||||||
|
|
||||||
|
# ✅ PYQT5 ENUMS
|
||||||
YES = QMessageBox.Yes
|
YES = QMessageBox.Yes
|
||||||
NO = QMessageBox.No
|
NO = QMessageBox.No
|
||||||
CANCEL = QMessageBox.Cancel
|
CANCEL = QMessageBox.Cancel
|
||||||
ICON_QUESTION = QMessageBox.Question
|
ICON_QUESTION = QMessageBox.Question
|
||||||
|
AcceptRole = QMessageBox.AcceptRole
|
||||||
|
ActionRole = QMessageBox.ActionRole
|
||||||
|
RejectRole = QMessageBox.RejectRole
|
||||||
|
|
||||||
QT_VERSION = 5
|
|
||||||
|
|
||||||
# then try next backend
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Qt5 Enum-Aliase (vereinheitlicht)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
|
# PyQt5 Enum-Aliase
|
||||||
ToolButtonTextBesideIcon = Qt.ToolButtonTextBesideIcon
|
ToolButtonTextBesideIcon = Qt.ToolButtonTextBesideIcon
|
||||||
ArrowDown = Qt.DownArrow
|
ArrowDown = Qt.DownArrow
|
||||||
ArrowRight = Qt.RightArrow
|
ArrowRight = Qt.RightArrow
|
||||||
# QSizePolicy Enum-Aliase (Qt5)
|
|
||||||
SizePolicyPreferred = QSizePolicy.Preferred
|
SizePolicyPreferred = QSizePolicy.Preferred
|
||||||
SizePolicyMaximum = QSizePolicy.Maximum
|
SizePolicyMaximum = QSizePolicy.Maximum
|
||||||
# ---------------------------------------------------------
|
|
||||||
# QDockWidget Feature-Aliase (Qt5)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
DockWidgetMovable = QDockWidget.DockWidgetMovable
|
DockWidgetMovable = QDockWidget.DockWidgetMovable
|
||||||
DockWidgetFloatable = QDockWidget.DockWidgetFloatable
|
DockWidgetFloatable = QDockWidget.DockWidgetFloatable
|
||||||
DockWidgetClosable = QDockWidget.DockWidgetClosable
|
DockWidgetClosable = QDockWidget.DockWidgetClosable
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Dock-Area-Aliase (Qt5)
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
DockAreaLeft = Qt.LeftDockWidgetArea
|
DockAreaLeft = Qt.LeftDockWidgetArea
|
||||||
DockAreaRight = Qt.RightDockWidgetArea
|
DockAreaRight = Qt.RightDockWidgetArea
|
||||||
|
|
||||||
|
|
||||||
def exec_dialog(dialog: Any) -> Any:
|
def exec_dialog(dialog: Any) -> Any:
|
||||||
return dialog.exec_()
|
return dialog.exec_()
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
print(f"✅ qt_wrapper: PyQt5 Fallback geladen (QT_VERSION={QT_VERSION})")
|
||||||
# Mock-Modus
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
|
# --------------------------- MOCK-MODUS ---------------------------
|
||||||
except Exception:
|
except Exception:
|
||||||
QT_VERSION = 0
|
QT_VERSION = 0
|
||||||
|
print("⚠️ qt_wrapper: Mock-Modus aktiviert (QT_VERSION=0)")
|
||||||
|
|
||||||
|
# Fake Enum für Bit-Operationen
|
||||||
class FakeEnum(int):
|
class FakeEnum(int):
|
||||||
def __or__(self, other: int) -> "FakeEnum":
|
def __or__(self, other: Any) -> "FakeEnum":
|
||||||
return FakeEnum(int(self) | int(other))
|
return FakeEnum(int(self) | int(other))
|
||||||
|
|
||||||
YES = FakeEnum(1)
|
YES = FakeEnum(1)
|
||||||
@@ -262,66 +281,266 @@ except Exception:
|
|||||||
CANCEL = FakeEnum(4)
|
CANCEL = FakeEnum(4)
|
||||||
ICON_QUESTION = FakeEnum(8)
|
ICON_QUESTION = FakeEnum(8)
|
||||||
|
|
||||||
|
# Im Mock-Block von qt_wrapper.py:
|
||||||
class _MockQMessageBox:
|
class _MockQMessageBox:
|
||||||
Yes = YES
|
Yes = YES
|
||||||
No = NO
|
No = NO
|
||||||
Cancel = CANCEL
|
Cancel = CANCEL
|
||||||
Question = ICON_QUESTION
|
Question = ICON_QUESTION
|
||||||
|
AcceptRole = 0
|
||||||
|
ActionRole = 3
|
||||||
|
RejectRole = 1
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def question(cls, parent, title, message, buttons, default_button):
|
||||||
|
"""Mock: Gibt immer default_button zurück"""
|
||||||
|
print(f"🔍 Mock QMessageBox.question: '{title}' → {default_button}")
|
||||||
|
return default_button
|
||||||
|
|
||||||
QMessageBox = _MockQMessageBox
|
QMessageBox = _MockQMessageBox
|
||||||
|
|
||||||
|
|
||||||
class _MockQFileDialog:
|
class _MockQFileDialog:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getOpenFileName(*args, **kwargs):
|
def getOpenFileName(*args, **kwargs): return ("", "")
|
||||||
return ("", "")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getSaveFileName(*args, **kwargs):
|
def getSaveFileName(*args, **kwargs): return ("", "")
|
||||||
return ("", "")
|
|
||||||
|
|
||||||
QFileDialog = _MockQFileDialog
|
QFileDialog = _MockQFileDialog
|
||||||
|
|
||||||
class _MockQEventLoop:
|
class _MockQInputDialog:
|
||||||
def exec(self) -> int:
|
@staticmethod
|
||||||
return 0
|
def getText(parent, title, label, mode=None, text=""):
|
||||||
|
return text, True
|
||||||
|
|
||||||
def quit(self) -> None:
|
QInputDialog = _MockQInputDialog
|
||||||
pass
|
|
||||||
|
class _MockQEventLoop:
|
||||||
|
def exec(self) -> int: return 0
|
||||||
|
def quit(self) -> None: pass
|
||||||
|
|
||||||
QEventLoop = _MockQEventLoop
|
QEventLoop = _MockQEventLoop
|
||||||
|
|
||||||
|
class _MockQTimer:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.timeout = type('Signal', (), {
|
||||||
|
'connect': lambda s, cb: None,
|
||||||
|
})()
|
||||||
|
def setSingleShot(self, v: bool) -> None: pass
|
||||||
|
def start(self, ms: int) -> None: pass
|
||||||
|
def stop(self) -> None: pass
|
||||||
|
|
||||||
|
QTimer = _MockQTimer
|
||||||
|
|
||||||
class _MockQUrl(str):
|
class _MockQUrl(str):
|
||||||
def isValid(self) -> bool:
|
def isValid(self) -> bool: return True
|
||||||
return True
|
|
||||||
|
|
||||||
QUrl = _MockQUrl
|
QUrl = _MockQUrl
|
||||||
|
|
||||||
class _MockQNetworkRequest:
|
class _MockQNetworkRequest:
|
||||||
def __init__(self, url: Any):
|
def __init__(self, url: Any): self.url = url
|
||||||
self.url = url
|
|
||||||
|
|
||||||
QNetworkRequest = _MockQNetworkRequest
|
QNetworkRequest = _MockQNetworkRequest
|
||||||
|
|
||||||
class _MockQNetworkReply:
|
class _MockQNetworkReply:
|
||||||
def error(self) -> int:
|
def error(self) -> int: return 0
|
||||||
return 0
|
def errorString(self) -> str: return ""
|
||||||
|
def readAll(self) -> bytes: return b""
|
||||||
def errorString(self) -> str:
|
def deleteLater(self) -> None: pass
|
||||||
return ""
|
|
||||||
|
|
||||||
def readAll(self) -> bytes:
|
|
||||||
return b""
|
|
||||||
|
|
||||||
def deleteLater(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
QNetworkReply = _MockQNetworkReply
|
QNetworkReply = _MockQNetworkReply
|
||||||
|
|
||||||
class _MockWidget:
|
class _MockWidget: pass
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class _MockLayout:
|
class _MockLayout:
|
||||||
|
def __init__(self, *args, **kwargs): self._widgets = []
|
||||||
|
def addWidget(self, widget): self._widgets.append(widget)
|
||||||
|
def addLayout(self, layout): pass
|
||||||
|
def addStretch(self, *args, **kwargs): pass
|
||||||
|
def setSpacing(self, *args, **kwargs): pass
|
||||||
|
def setContentsMargins(self, *args, **kwargs): pass
|
||||||
|
|
||||||
|
class _MockLabel:
|
||||||
|
def __init__(self, text: str = ""): self._text = text
|
||||||
|
class _MockLineEdit:
|
||||||
|
Normal = 0
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs): self._text = ""
|
||||||
|
def text(self) -> str: return self._text
|
||||||
|
def setText(self, value: str) -> None: self._text = value
|
||||||
|
|
||||||
|
class _MockFont:
|
||||||
|
def __init__(self, family: str = "", pointSize: int = 10):
|
||||||
|
self.family = family
|
||||||
|
self.pointSize = pointSize
|
||||||
|
|
||||||
|
class _MockButton:
|
||||||
|
def __init__(self, *args, **kwargs): self.clicked = lambda *a, **k: None
|
||||||
|
|
||||||
|
QWidget = _MockWidget
|
||||||
|
QGridLayout = _MockLayout
|
||||||
|
QLabel = _MockLabel
|
||||||
|
QLineEdit = _MockLineEdit
|
||||||
|
QGroupBox = _MockWidget
|
||||||
|
QVBoxLayout = _MockLayout
|
||||||
|
QPushButton = _MockButton
|
||||||
|
QFont = _MockFont
|
||||||
|
QCoreApplication = object()
|
||||||
|
|
||||||
|
class _MockQt:
|
||||||
|
ToolButtonTextBesideIcon = 0
|
||||||
|
ArrowDown = 1
|
||||||
|
ArrowRight = 2
|
||||||
|
LeftDockWidgetArea = 1
|
||||||
|
RightDockWidgetArea = 2
|
||||||
|
|
||||||
|
Qt = _MockQt()
|
||||||
|
ToolButtonTextBesideIcon = Qt.ToolButtonTextBesideIcon
|
||||||
|
ArrowDown = Qt.ArrowDown
|
||||||
|
ArrowRight = Qt.ArrowRight
|
||||||
|
DockAreaLeft = Qt.LeftDockWidgetArea
|
||||||
|
DockAreaRight = Qt.RightDockWidgetArea
|
||||||
|
|
||||||
|
class _MockQDockWidget(_MockWidget):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._object_name = ""
|
||||||
|
def setObjectName(self, name: str) -> None: self._object_name = name
|
||||||
|
def objectName(self) -> str: return self._object_name
|
||||||
|
def show(self) -> None: pass
|
||||||
|
def deleteLater(self) -> None: pass
|
||||||
|
|
||||||
|
QDockWidget = _MockQDockWidget
|
||||||
|
|
||||||
|
class _MockAction:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._checked = False
|
||||||
|
self.triggered = lambda *a, **k: None
|
||||||
|
def setToolTip(self, text: str) -> None: pass
|
||||||
|
def setCheckable(self, value: bool) -> None: pass
|
||||||
|
def setChecked(self, value: bool) -> None: self._checked = value
|
||||||
|
|
||||||
|
class _MockMenu:
|
||||||
|
def __init__(self, *args, **kwargs): self._actions = []
|
||||||
|
def addAction(self, action): self._actions.append(action)
|
||||||
|
def removeAction(self, action):
|
||||||
|
if action in self._actions: self._actions.remove(action)
|
||||||
|
def clear(self): self._actions.clear()
|
||||||
|
def menuAction(self): return self
|
||||||
|
|
||||||
|
class _MockToolBar:
|
||||||
|
def __init__(self, *args, **kwargs): self._actions = []
|
||||||
|
def setObjectName(self, name: str) -> None: pass
|
||||||
|
def addAction(self, action): self._actions.append(action)
|
||||||
|
def removeAction(self, action):
|
||||||
|
if action in self._actions: self._actions.remove(action)
|
||||||
|
def clear(self): self._actions.clear()
|
||||||
|
|
||||||
|
class _MockActionGroup:
|
||||||
|
def __init__(self, *args, **kwargs): self._actions = []
|
||||||
|
def setExclusive(self, value: bool) -> None: pass
|
||||||
|
def addAction(self, action): self._actions.append(action)
|
||||||
|
|
||||||
|
QAction = _MockAction
|
||||||
|
QMenu = _MockMenu
|
||||||
|
QToolBar = _MockToolBar
|
||||||
|
QActionGroup = _MockActionGroup
|
||||||
|
|
||||||
|
class _MockToolButton(_MockWidget):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._checked = False
|
||||||
|
self.toggled = lambda *a, **k: None
|
||||||
|
def setText(self, text: str) -> None: pass
|
||||||
|
def setCheckable(self, value: bool) -> None: pass
|
||||||
|
def setChecked(self, value: bool) -> None: self._checked = value
|
||||||
|
def setToolButtonStyle(self, *args, **kwargs): pass
|
||||||
|
def setArrowType(self, *args, **kwargs): pass
|
||||||
|
def setStyleSheet(self, *args, **kwargs): pass
|
||||||
|
|
||||||
|
QToolButton = _MockToolButton
|
||||||
|
|
||||||
|
class _MockQSizePolicy:
|
||||||
|
Preferred = 3
|
||||||
|
Maximum = 2
|
||||||
|
|
||||||
|
QSizePolicy = _MockQSizePolicy
|
||||||
|
SizePolicyPreferred = QSizePolicy.Preferred
|
||||||
|
SizePolicyMaximum = QSizePolicy.Maximum
|
||||||
|
DockWidgetMovable = 1
|
||||||
|
DockWidgetFloatable = 2
|
||||||
|
DockWidgetClosable = 4
|
||||||
|
|
||||||
|
class _MockTabWidget:
|
||||||
|
def __init__(self, *args, **kwargs): self._tabs = []
|
||||||
|
def addTab(self, widget, title: str): self._tabs.append((widget, title))
|
||||||
|
|
||||||
|
QTabWidget = _MockTabWidget
|
||||||
|
|
||||||
|
class _MockComboBox:
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
self._items = []
|
||||||
|
self._index = -1
|
||||||
|
self.currentTextChanged = type('Signal', (), {'connect': lambda s, cb: None, 'emit': lambda s, v: None})()
|
||||||
|
def addItem(self, text: str) -> None: self._items.append(text)
|
||||||
|
def addItems(self, items): [self.addItem(it) for it in items]
|
||||||
|
def findText(self, text: str) -> int:
|
||||||
|
return self._items.index(text) if text in self._items else -1
|
||||||
|
def setCurrentIndex(self, idx: int) -> None:
|
||||||
|
if 0 <= idx < len(self._items):
|
||||||
|
self._index = idx
|
||||||
|
self.currentTextChanged.emit(self.currentText())
|
||||||
|
def setCurrentText(self, text: str) -> None:
|
||||||
|
idx = self.findText(text)
|
||||||
|
if idx >= 0: self.setCurrentIndex(idx)
|
||||||
|
def currentText(self) -> str:
|
||||||
|
return self._items[self._index] if 0 <= self._index < len(self._items) else ""
|
||||||
|
|
||||||
|
QComboBox = _MockComboBox
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------
|
||||||
|
# Mock für QVariant
|
||||||
|
# ---------------------------
|
||||||
|
|
||||||
|
class _MockQVariant:
|
||||||
|
"""
|
||||||
|
Minimaler Ersatz für QtCore.QVariant.
|
||||||
|
|
||||||
|
Ziel:
|
||||||
|
- Werte transparent durchreichen
|
||||||
|
- Typ-Konstanten bereitstellen
|
||||||
|
- Keine Qt-Abhängigkeiten
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Typ-Konstanten (symbolisch, Werte egal)
|
||||||
|
Invalid = 0
|
||||||
|
Int = 1
|
||||||
|
Double = 2
|
||||||
|
String = 3
|
||||||
|
Bool = 4
|
||||||
|
Date = 5
|
||||||
|
DateTime = 6
|
||||||
|
|
||||||
|
def __init__(self, value: Any = None):
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
def value(self) -> Any:
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"QVariant({self._value!r})"
|
||||||
|
|
||||||
|
# Optional: automatische Entpackung
|
||||||
|
def __int__(self):
|
||||||
|
return int(self._value)
|
||||||
|
|
||||||
|
def __float__(self):
|
||||||
|
return float(self._value)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self._value)
|
||||||
|
QVariant = _MockQVariant
|
||||||
|
|
||||||
|
class _MockQHBoxLayout:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self._widgets = []
|
self._widgets = []
|
||||||
|
|
||||||
@@ -339,242 +558,25 @@ except Exception:
|
|||||||
|
|
||||||
def setContentsMargins(self, *args, **kwargs):
|
def setContentsMargins(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
QHBoxLayout = _MockQHBoxLayout
|
||||||
|
|
||||||
|
class _MockQCheckBox:
|
||||||
|
def __init__(self, text: str = "", *args, **kwargs):
|
||||||
class _MockLabel:
|
|
||||||
def __init__(self, text: str = ""):
|
|
||||||
self._text = text
|
self._text = text
|
||||||
|
|
||||||
class _MockLineEdit:
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self._text = ""
|
|
||||||
|
|
||||||
def text(self) -> str:
|
|
||||||
return self._text
|
|
||||||
|
|
||||||
def setText(self, value: str) -> None:
|
|
||||||
self._text = value
|
|
||||||
|
|
||||||
class _MockButton:
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.clicked = lambda *a, **k: None
|
|
||||||
|
|
||||||
QWidget = _MockWidget
|
|
||||||
QGridLayout = _MockLayout
|
|
||||||
QLabel = _MockLabel
|
|
||||||
QLineEdit = _MockLineEdit
|
|
||||||
QGroupBox = _MockWidget
|
|
||||||
QVBoxLayout = _MockLayout
|
|
||||||
QPushButton = _MockButton
|
|
||||||
|
|
||||||
class _MockQCoreApplication:
|
|
||||||
pass
|
|
||||||
|
|
||||||
QCoreApplication = _MockQCoreApplication
|
|
||||||
class _MockQt:
|
|
||||||
# ToolButtonStyle
|
|
||||||
ToolButtonTextBesideIcon = 0
|
|
||||||
|
|
||||||
# ArrowType
|
|
||||||
ArrowDown = 1
|
|
||||||
ArrowRight = 2
|
|
||||||
|
|
||||||
Qt=_MockQt
|
|
||||||
ToolButtonTextBesideIcon = Qt.ToolButtonTextBesideIcon
|
|
||||||
ArrowDown = Qt.ArrowDown
|
|
||||||
ArrowRight = Qt.ArrowRight
|
|
||||||
|
|
||||||
class _MockQDockWidget(_MockWidget):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self._object_name = ""
|
|
||||||
|
|
||||||
def setObjectName(self, name: str) -> None:
|
|
||||||
self._object_name = name
|
|
||||||
|
|
||||||
def objectName(self) -> str:
|
|
||||||
return self._object_name
|
|
||||||
|
|
||||||
def show(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def deleteLater(self) -> None:
|
|
||||||
pass
|
|
||||||
QDockWidget = _MockQDockWidget
|
|
||||||
class _MockAction:
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self._checked = False
|
self._checked = False
|
||||||
self.triggered = lambda *a, **k: None
|
|
||||||
|
|
||||||
def setToolTip(self, text: str) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def setCheckable(self, value: bool) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def setChecked(self, value: bool) -> None:
|
|
||||||
self._checked = value
|
|
||||||
|
|
||||||
|
|
||||||
class _MockMenu:
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self._actions = []
|
|
||||||
|
|
||||||
def addAction(self, action):
|
|
||||||
self._actions.append(action)
|
|
||||||
|
|
||||||
def removeAction(self, action):
|
|
||||||
if action in self._actions:
|
|
||||||
self._actions.remove(action)
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self._actions.clear()
|
|
||||||
|
|
||||||
def menuAction(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class _MockToolBar:
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self._actions = []
|
|
||||||
|
|
||||||
def setObjectName(self, name: str) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def addAction(self, action):
|
|
||||||
self._actions.append(action)
|
|
||||||
|
|
||||||
def removeAction(self, action):
|
|
||||||
if action in self._actions:
|
|
||||||
self._actions.remove(action)
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self._actions.clear()
|
|
||||||
|
|
||||||
|
|
||||||
class _MockActionGroup:
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self._actions = []
|
|
||||||
|
|
||||||
def setExclusive(self, value: bool) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def addAction(self, action):
|
|
||||||
self._actions.append(action)
|
|
||||||
QAction = _MockAction
|
|
||||||
QMenu = _MockMenu
|
|
||||||
QToolBar = _MockToolBar
|
|
||||||
QActionGroup = _MockActionGroup
|
|
||||||
|
|
||||||
class _MockToolButton(_MockWidget):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self._checked = False
|
|
||||||
self.toggled = lambda *a, **k: None
|
|
||||||
|
|
||||||
def setText(self, text: str) -> None:
|
def setText(self, text: str) -> None:
|
||||||
pass
|
self._text = text
|
||||||
|
|
||||||
def setCheckable(self, value: bool) -> None:
|
def isChecked(self) -> bool:
|
||||||
pass
|
return self._checked
|
||||||
|
|
||||||
def setChecked(self, value: bool) -> None:
|
def setChecked(self, checked: bool) -> None:
|
||||||
self._checked = value
|
self._checked = checked
|
||||||
|
|
||||||
def setToolButtonStyle(self, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def setArrowType(self, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def setStyleSheet(self, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
QToolButton=_MockToolButton
|
|
||||||
|
|
||||||
class _MockQSizePolicy:
|
|
||||||
# horizontale Policies
|
|
||||||
Fixed = 0
|
|
||||||
Minimum = 1
|
|
||||||
Maximum = 2
|
|
||||||
Preferred = 3
|
|
||||||
Expanding = 4
|
|
||||||
MinimumExpanding = 5
|
|
||||||
Ignored = 6
|
|
||||||
|
|
||||||
# vertikale Policies (Qt nutzt dieselben Werte)
|
|
||||||
def __init__(self, horizontal=None, vertical=None):
|
|
||||||
self.horizontal = horizontal
|
|
||||||
self.vertical = vertical
|
|
||||||
QSizePolicy=_MockQSizePolicy
|
|
||||||
SizePolicyPreferred = QSizePolicy.Preferred
|
|
||||||
SizePolicyMaximum = QSizePolicy.Maximum
|
|
||||||
DockWidgetMovable = 1
|
|
||||||
DockWidgetFloatable = 2
|
|
||||||
DockWidgetClosable = 4
|
|
||||||
DockAreaLeft = 1
|
|
||||||
DockAreaRight = 2
|
|
||||||
|
|
||||||
|
QCheckBox = _MockQCheckBox
|
||||||
def exec_dialog(dialog: Any) -> Any:
|
def exec_dialog(dialog: Any) -> Any:
|
||||||
return YES
|
return YES
|
||||||
class _MockTabWidget:
|
# --------------------------- TEST ---------------------------
|
||||||
def __init__(self, *args, **kwargs):
|
if __name__ == "__main__":
|
||||||
self._tabs = []
|
debug_qt_status()
|
||||||
|
|
||||||
def addTab(self, widget, title: str):
|
|
||||||
self._tabs.append((widget, title))
|
|
||||||
QTabWidget = _MockTabWidget
|
|
||||||
|
|
||||||
# -------------------------
|
|
||||||
# Mock ComboBox Implementation
|
|
||||||
# -------------------------
|
|
||||||
class _MockSignal:
|
|
||||||
def __init__(self):
|
|
||||||
self._slots = []
|
|
||||||
|
|
||||||
def connect(self, cb):
|
|
||||||
self._slots.append(cb)
|
|
||||||
|
|
||||||
def emit(self, value):
|
|
||||||
for s in list(self._slots):
|
|
||||||
try:
|
|
||||||
s(value)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
class _MockComboBox:
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
self._items = []
|
|
||||||
self._index = -1
|
|
||||||
self.currentTextChanged = _MockSignal()
|
|
||||||
|
|
||||||
def addItem(self, text: str) -> None:
|
|
||||||
self._items.append(text)
|
|
||||||
|
|
||||||
def addItems(self, items):
|
|
||||||
for it in items:
|
|
||||||
self.addItem(it)
|
|
||||||
|
|
||||||
def findText(self, text: str) -> int:
|
|
||||||
try:
|
|
||||||
return self._items.index(text)
|
|
||||||
except ValueError:
|
|
||||||
return -1
|
|
||||||
|
|
||||||
def setCurrentIndex(self, idx: int) -> None:
|
|
||||||
if 0 <= idx < len(self._items):
|
|
||||||
self._index = idx
|
|
||||||
self.currentTextChanged.emit(self.currentText())
|
|
||||||
|
|
||||||
def setCurrentText(self, text: str) -> None:
|
|
||||||
idx = self.findText(text)
|
|
||||||
if idx >= 0:
|
|
||||||
self.setCurrentIndex(idx)
|
|
||||||
|
|
||||||
def currentText(self) -> str:
|
|
||||||
if 0 <= self._index < len(self._items):
|
|
||||||
return self._items[self._index]
|
|
||||||
return ""
|
|
||||||
|
|
||||||
ComboBox = _MockComboBox
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
from typing import Union
|
from typing import Union
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from sn_basis.functions.os_wrapper import is_absolute_path, basename
|
||||||
|
|
||||||
_PathLike = Union[str, Path]
|
_PathLike = Union[str, Path]
|
||||||
|
|
||||||
|
|||||||
21
metadata.txt
21
metadata.txt
@@ -1,13 +1,14 @@
|
|||||||
[general]
|
[general]
|
||||||
name=LNO Sachsen | Basisfunktionen
|
name=LNO Sachsen | Plugin Basisfunktionen
|
||||||
qgisMinimumVersion=3.0
|
qgisMinimumVersion=3.40
|
||||||
|
qgisMaximumVersion=3.99
|
||||||
description=Plugin mit Basisfunktionen
|
description=Plugin mit Basisfunktionen
|
||||||
version=25.11.4
|
version=26.3.6-unstable
|
||||||
author=Michael Otto
|
author=Daniel Helbig
|
||||||
email=michael.otto@landkreis-mittelsachsen.de
|
email=daniel.helbig@kreis-meissen.de
|
||||||
about=Plugin mit Basisfunktionen
|
homepage=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis
|
||||||
category=Plugins
|
tracker=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis/issues
|
||||||
homepage=https://entwicklung.vln-sn.de/AG_QGIS/Plugin_SN_Basis
|
repository=https://entwicklung.flurneuordnung-sachsen.de/AG_QGIS/Plugin_SN_Basis/src/branch/unstable/
|
||||||
repository=https://entwicklung.vln-sn.de/AG_QGIS/Repository
|
|
||||||
supportsQt6=true
|
|
||||||
experimental=true
|
experimental=true
|
||||||
|
deprecated=false
|
||||||
|
supportsQt6=true
|
||||||
@@ -1,424 +1,351 @@
|
|||||||
# sn_basis/modules/DataGrabber.py
|
|
||||||
"""
|
"""
|
||||||
DataGrabber module
|
DataGrabber module
|
||||||
==================
|
==================
|
||||||
|
|
||||||
Leichter Orchestrator, der eine Quelle (Datei, Einzellink, Datenbank)
|
UI-freier Orchestrator für die Prüfung und Klassifikation von Datenquellen.
|
||||||
analysiert, passende Prüfer aufruft und die Ergebnisse an den
|
|
||||||
:class:`sn_basis.modules.Pruefmanager.Pruefmanager` delegiert.
|
|
||||||
|
|
||||||
Dieses vereinfachte Modul geht davon aus, dass alle benötigten Prüfer
|
Der DataGrabber:
|
||||||
und der ExcelImporter vorhanden und importierbar sind. Es enthält
|
- klassifiziert die übergebene Quelle (Datei, Dienst, Datenbank, Excel),
|
||||||
keine Fallbacks oder defensive Exception-Handling-Pfade für fehlende
|
- ruft passende Prüfer (Dateipruefer, Linkpruefer, Layerpruefer, Stilpruefer) auf,
|
||||||
Prüfer-Module — fehlende Komponenten führen zu Import- oder Laufzeitfehlern,
|
- sammelt alle rohen ``pruef_ergebnis``-Objekte,
|
||||||
die bewusst nicht unterdrückt werden.
|
- aggregiert diese zu einem zusammenfassenden Ergebnis,
|
||||||
|
- **löst selbst keinerlei UI-Interaktion aus**.
|
||||||
|
|
||||||
|
Alle Nutzerinteraktionen (MessageBar, QMessageBox, Logging) erfolgen
|
||||||
|
ausschließlich über den ``Pruefmanager`` im aufrufenden Kontext (UI / Pipeline).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import (
|
import re
|
||||||
Optional,
|
from typing import Any, Dict, List, Mapping, Optional, Tuple, Literal
|
||||||
Any,
|
|
||||||
Mapping,
|
|
||||||
Iterable,
|
|
||||||
Dict,
|
|
||||||
Protocol,
|
|
||||||
Literal,
|
|
||||||
Tuple,
|
|
||||||
List,
|
|
||||||
)
|
|
||||||
from pathlib import Path
|
|
||||||
import sqlite3
|
|
||||||
|
|
||||||
|
from sn_basis.functions.os_wrapper import basename, path_suffix
|
||||||
|
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
from sn_basis.modules.Pruefmanager import Pruefmanager
|
from sn_basis.modules.Pruefmanager import Pruefmanager
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
|
|
||||||
|
|
||||||
# In dieser vereinfachten Variante werden die Prüfer und der ExcelImporter
|
|
||||||
# direkt importiert. Fehlende Module führen zu ImportError (gewollt).
|
|
||||||
from sn_basis.modules.Dateipruefer import Dateipruefer
|
from sn_basis.modules.Dateipruefer import Dateipruefer
|
||||||
from sn_basis.modules.linkpruefer import Linkpruefer
|
from sn_basis.modules.linkpruefer import Linkpruefer
|
||||||
from sn_basis.modules.layerpruefer import Layerpruefer
|
from sn_basis.modules.layerpruefer import Layerpruefer
|
||||||
from sn_basis.modules.stilpruefer import Stilpruefer
|
from sn_basis.modules.stilpruefer import Stilpruefer
|
||||||
from sn_basis.modules.excel_importer import ExcelImporter
|
from sn_basis.modules.excel_importer import ExcelImporter
|
||||||
|
from sn_plan41.modules.listenauswerter import Listenauswerter
|
||||||
|
|
||||||
|
|
||||||
SourceType = Literal["file", "link", "database", "unknown"]
|
SourceType = Literal["service", "database", "excel", "unknown"]
|
||||||
|
|
||||||
|
SourceDict = Dict[str, List[Mapping[str, Any]]]
|
||||||
class LinklistAdapter(Protocol):
|
|
||||||
"""
|
|
||||||
Minimal-Protokoll für Adapter, die Linklisten liefern/normalisieren.
|
|
||||||
|
|
||||||
Implementierende Klassen sollten:
|
|
||||||
- load() -> Iterable[Mapping[str, Any]]
|
|
||||||
- normalize(raw_item) -> Mapping[str, Any]
|
|
||||||
"""
|
|
||||||
def load(self) -> Iterable[Mapping[str, Any]]:
|
|
||||||
...
|
|
||||||
def normalize(self, raw_item: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class DataGrabber:
|
class DataGrabber:
|
||||||
"""
|
"""
|
||||||
DataGrabber orchestriert das Einlesen einer Quelle und die Übergabe an Prüfer.
|
Analysiert und prüft Datenquellen für den Fachdatenabruf.
|
||||||
|
|
||||||
Diese vereinfachte Implementierung erwartet, dass alle Prüferklassen und
|
|
||||||
der ExcelImporter vorhanden sind. Es gibt keine defensive Logik für
|
|
||||||
fehlende Komponenten.
|
|
||||||
|
|
||||||
Konstruktor-Parameter
|
|
||||||
--------------------
|
|
||||||
:param pruefmanager: Instanz des Pruefmanagers (verpflichtend).
|
|
||||||
:param datei_pruefer_cls: Klasse des Dateipruefers (Standard: Dateipruefer).
|
|
||||||
:param link_pruefer: Instanz des Linkpruefers.
|
|
||||||
:param layer_pruefer: Instanz des Layerpruefers.
|
|
||||||
:param stil_pruefer: Instanz des Stilpruefers.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
pruefmanager: Pruefmanager,
|
pruefmanager: Pruefmanager,
|
||||||
*,
|
*,
|
||||||
datei_pruefer_cls=Dateipruefer,
|
datei_pruefer_cls: type[Dateipruefer] = Dateipruefer,
|
||||||
link_pruefer: Linkpruefer,
|
link_pruefer: Optional[Linkpruefer] = None,
|
||||||
layer_pruefer: Layerpruefer,
|
layer_pruefer: Optional[Layerpruefer] = None,
|
||||||
stil_pruefer: Stilpruefer,
|
stil_pruefer: Optional[Stilpruefer] = None,
|
||||||
|
excel_importer_cls: type[ExcelImporter] = ExcelImporter,
|
||||||
) -> None:
|
) -> None:
|
||||||
# Pruefmanager ist verpflichtend
|
self.pruefmanager = pruefmanager
|
||||||
self.pruefmanager: Pruefmanager = pruefmanager
|
|
||||||
|
|
||||||
# Dateipruefer-Klasse (wird zur Laufzeit mit einem Pfad instanziert)
|
|
||||||
self._datei_pruefer_cls = datei_pruefer_cls
|
self._datei_pruefer_cls = datei_pruefer_cls
|
||||||
|
self.link_pruefer = link_pruefer or Linkpruefer()
|
||||||
|
self.layer_pruefer = layer_pruefer or Layerpruefer()
|
||||||
|
self.stil_pruefer = stil_pruefer or Stilpruefer()
|
||||||
|
self._excel_importer_cls = excel_importer_cls
|
||||||
|
|
||||||
# Prüfer-Instanzen (werden direkt verwendet)
|
self._source: Optional[str] = None
|
||||||
self.link_pruefer: Linkpruefer = link_pruefer
|
|
||||||
self.layer_pruefer: Layerpruefer = layer_pruefer
|
|
||||||
self.stil_pruefer: Stilpruefer = stil_pruefer
|
|
||||||
|
|
||||||
# Quelle (wird später gesetzt)
|
# ------------------------------------------------------------------
|
||||||
self.source: Optional[str] = None
|
# Öffentliche API
|
||||||
|
# ------------------------------------------------------------------
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# Source Management
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
def set_source(self, source: str) -> None:
|
def set_source(self, source: str) -> None:
|
||||||
|
"""Setzt die aktuell zu untersuchende Rohquelle."""
|
||||||
|
self._source = source
|
||||||
|
|
||||||
|
SourceType = str # "excel" | "datenbank" | "dienst" | "unbekannt"
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_source_type(self, quelle: str) -> Tuple[SourceType, pruef_ergebnis]:
|
||||||
"""
|
"""
|
||||||
Setzt die Quelle für den DataGrabber.
|
Klassifiziert die Quelle und liefert das zugehörige pruef_ergebnis.
|
||||||
|
|
||||||
Die Quelle ist ein String, der entweder ein lokaler Dateipfad,
|
Reihenfolge:
|
||||||
ein Einzellink (URL/URI) oder ein Pfad zu einer Datenbank/GeoPackage ist.
|
1. Dateipruefer (Datei + Dateityp)
|
||||||
"""
|
2. Linkpruefer (Dienst)
|
||||||
self.source = source
|
|
||||||
|
|
||||||
def analyze_source(self, source: str) -> SourceType:
|
|
||||||
"""
|
|
||||||
Klassifiziert die angegebene Quelle ausschließlich anhand des Dateipruefers.
|
|
||||||
|
|
||||||
Ablauf
|
|
||||||
------
|
|
||||||
1. Instanziere den Dateipruefer mit `pfad=source` und `temporaer_erlaubt=False`.
|
|
||||||
2. Rufe `pruefe()` auf und werte das zurückgegebene :class:`pruef_ergebnis` aus.
|
|
||||||
3. Bei `ok==True` wird anhand der Dateiendung zwischen "database" (gpkg/sqlite/db)
|
|
||||||
und "file" unterschieden.
|
|
||||||
4. Bei `ok==False` werden typische Aktionen wie "datei_nicht_gefunden" als "link"
|
|
||||||
interpretiert; bei "falsche_endung" wird anhand der Endung klassifiziert.
|
|
||||||
"""
|
|
||||||
dp = self._datei_pruefer_cls(pfad=source, temporaer_erlaubt=False)
|
|
||||||
pe: pruef_ergebnis = dp.pruefe()
|
|
||||||
|
|
||||||
if getattr(pe, "ok", False):
|
|
||||||
suffix = Path(source).suffix.lower()
|
|
||||||
if suffix in (".gpkg", ".sqlite", ".db"):
|
|
||||||
return "database"
|
|
||||||
return "file"
|
|
||||||
|
|
||||||
aktion = getattr(pe, "aktion", None)
|
|
||||||
if aktion in ("datei_nicht_gefunden", "pfad_nicht_gefunden", "kein_dateipfad"):
|
|
||||||
return "link"
|
|
||||||
if aktion == "falsche_endung":
|
|
||||||
lower = source.lower()
|
|
||||||
for db_ext in (".gpkg", ".sqlite", ".db"):
|
|
||||||
if lower.endswith(db_ext):
|
|
||||||
return "database"
|
|
||||||
for file_ext in (".xlsx", ".xls", ".csv"):
|
|
||||||
if lower.endswith(file_ext):
|
|
||||||
return "file"
|
|
||||||
return "unknown"
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# Excel-Verarbeitung
|
|
||||||
#Es werden alle Werte mit gültigem Link übernommen. Die restliche Struktur
|
|
||||||
#wird nicht überprüft, da alle Fachplugins unterschiedliche Strukturen haben können
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
def process_excel_source(
|
|
||||||
self,
|
|
||||||
filepath: str
|
|
||||||
) -> Tuple[Optional[Dict[str, List[Mapping[str, Any]]]], Any]:
|
|
||||||
"""
|
|
||||||
Liest eine Excel-Datei ein und übernimmt ausschließlich die Zeilen,
|
|
||||||
deren Link durch den Linkpruefer als gültig eingestuft wurde.
|
|
||||||
|
|
||||||
Ablauf
|
|
||||||
------
|
|
||||||
1. Die Excel-Datei wird mit dem ``ExcelImporter`` eingelesen.
|
|
||||||
Erwartet wird eine Liste von Mappings (z.B. dicts), die jeweils
|
|
||||||
die Linkparameter enthalten.
|
|
||||||
|
|
||||||
2. Für jede Zeile wird der Wert ``row["Link"]`` extrahiert und durch
|
|
||||||
``self.link_pruefer.pruefe(...)`` geprüft.
|
|
||||||
|
|
||||||
3. Das Prüfergebnis wird durch ``self.pruefmanager.verarbeite(...)``
|
|
||||||
geleitet, der UI-Interaktion, Logging und finale Entscheidung übernimmt.
|
|
||||||
|
|
||||||
4. Nur Zeilen, deren verarbeitete Prüfergebnisse ``ok == True`` liefern,
|
|
||||||
werden in die Ergebnisliste übernommen.
|
|
||||||
|
|
||||||
5. Wenn mindestens eine Zeile gültig ist, wird ein Dict der Form::
|
|
||||||
|
|
||||||
{"rows": [row1, row2, ...]}
|
|
||||||
|
|
||||||
zurückgegeben.
|
|
||||||
Wenn keine Zeile gültig ist, wird ``None`` zurückgegeben.
|
|
||||||
|
|
||||||
Parameter
|
|
||||||
---------
|
|
||||||
filepath:
|
|
||||||
Pfad zur Excel-Datei, die eingelesen werden soll.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
Tuple[Optional[Dict[str, List[Mapping[str, Any]]]], pruef_ergebnis]
|
|
||||||
- ``data``: ``{"rows": [...]} `` wenn gültige Zeilen existieren,
|
|
||||||
sonst ``None``.
|
|
||||||
- ``pruef_ergebnis``: ein zusammenfassendes Prüfergebnis, das
|
|
||||||
den Lesevorgang beschreibt (nicht die Einzelprüfungen).
|
|
||||||
|
|
||||||
Hinweise
|
|
||||||
--------
|
|
||||||
- Diese Methode führt **keine Normalisierung** durch.
|
|
||||||
- Die Verantwortung für die Struktur der Excel-Zeilen liegt beim Fachplugin.
|
|
||||||
- Der Linkpruefer prüft ausschließlich den Wert ``row["Link"]``.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 1) Excel einlesen
|
# --------------------------------------------------
|
||||||
importer = ExcelImporter(filepath=filepath, pruefmanager=self.pruefmanager)
|
# 1. Datei prüfen (inkl. Typ-Erkennung)
|
||||||
rows = importer.import_xlsx() # erwartet: List[Mapping[str, Any]]
|
# --------------------------------------------------
|
||||||
|
dateipruefer = Dateipruefer(pfad=quelle)
|
||||||
|
datei_ergebnis = dateipruefer.pruefe()
|
||||||
|
|
||||||
valid_rows: List[Mapping[str, Any]] = []
|
if datei_ergebnis.ok:
|
||||||
|
suffix = path_suffix(datei_ergebnis.kontext).lower()
|
||||||
|
print(f"[DataGrabber] Debug: analyze_source_type source={quelle} -> suffix={suffix}")
|
||||||
|
|
||||||
# 2) Jede Zeile einzeln prüfen
|
if suffix == ".xlsx":
|
||||||
for row in rows:
|
return "excel", datei_ergebnis
|
||||||
raw_link = row.get("Link")
|
|
||||||
|
|
||||||
# 2a) Fachliche Prüfung
|
if suffix in (".gpkg", ".sqlite"):
|
||||||
pe = self.link_pruefer.pruefe(raw_link)
|
return "datenbank", datei_ergebnis
|
||||||
|
|
||||||
# 2b) Verarbeitung durch den Pruefmanager
|
return "unbekannter_dateityp", datei_ergebnis
|
||||||
processed = self.pruefmanager.verarbeite(pe)
|
|
||||||
|
|
||||||
# 2c) Nur gültige Zeilen übernehmen
|
# --------------------------------------------------
|
||||||
if getattr(processed, "ok", False):
|
# 2. Keine Datei → Link prüfen
|
||||||
valid_rows.append(row)
|
# --------------------------------------------------
|
||||||
|
linkpruefer = Linkpruefer()
|
||||||
|
link_ergebnis = linkpruefer.pruefe(quelle)
|
||||||
|
|
||||||
# 3) Zusammenfassendes Prüfergebnis erzeugen
|
if link_ergebnis.ok:
|
||||||
if valid_rows:
|
return "dienst", link_ergebnis
|
||||||
pe_ok = pruef_ergebnis(
|
|
||||||
ok=True,
|
|
||||||
meldung=f"{len(valid_rows)} gültige Zeilen aus Excel gelesen",
|
|
||||||
aktion="ok",
|
|
||||||
kontext=filepath,
|
|
||||||
)
|
|
||||||
processed_summary = self.pruefmanager.verarbeite(pe_ok)
|
|
||||||
return {"rows": valid_rows}, processed_summary
|
|
||||||
|
|
||||||
# Keine gültigen Zeilen
|
# --------------------------------------------------
|
||||||
pe_fail = pruef_ergebnis(
|
# 3. Weder Datei noch Dienst
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
return "unbekannte_quelle", link_ergebnis
|
||||||
|
|
||||||
|
def run(self, source: str) -> Tuple[SourceDict, pruef_ergebnis]:
|
||||||
|
"""
|
||||||
|
Führt die vollständige Quellprüfung aus.
|
||||||
|
|
||||||
|
Diese Methode ist **UIfrei**. Sie gibt rohe Ergebnisse zurück,
|
||||||
|
die vom Aufrufer über den ``Pruefmanager`` verarbeitet werden.
|
||||||
|
"""
|
||||||
|
self.set_source(source)
|
||||||
|
source_type, source_result = self.analyze_source_type(source)
|
||||||
|
print(f"[DataGrabber] Debug: run source={source} -> source_type={source_type}")
|
||||||
|
|
||||||
|
source_dict: SourceDict = {}
|
||||||
|
partial_results: List[pruef_ergebnis] = []
|
||||||
|
|
||||||
|
if source_type == "excel":
|
||||||
|
source_dict, partial_results = self._process_excel_source(source)
|
||||||
|
elif source_type == "database":
|
||||||
|
source_dict, partial_results = self._process_database_source(source)
|
||||||
|
elif source_type == "service":
|
||||||
|
source_dict, partial_results = self._process_service_source(source)
|
||||||
|
else:
|
||||||
|
partial_results.append(source_result)
|
||||||
|
|
||||||
|
summary = self._aggregate_results(source, source_dict, partial_results)
|
||||||
|
return source_dict, summary
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Excel‑Quellen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _process_excel_source(
|
||||||
|
self, filepath: str
|
||||||
|
) -> Tuple[SourceDict, List[pruef_ergebnis]]:
|
||||||
|
source_dict: SourceDict = {"rows": []}
|
||||||
|
results: List[pruef_ergebnis] = []
|
||||||
|
|
||||||
|
rows = ExcelImporter(filepath, self.pruefmanager).import_xlsx()
|
||||||
|
print(f"[DataGrabber] Debug: Excel-Linkliste geladen: {filepath}")
|
||||||
|
print(f"[DataGrabber] Debug: raw rows count: {len(rows)}")
|
||||||
|
if rows:
|
||||||
|
first = rows[:min(5, len(rows))]
|
||||||
|
print(f"[DataGrabber] Debug: first rows: {first}")
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
return source_dict, results
|
||||||
|
|
||||||
|
required_keys = {"ident", "gruppe", "kartenebene", "inhalt", "link", "provider", "stildatei"}
|
||||||
|
|
||||||
|
def extract_url(raw_link: str, provider: str) -> str:
|
||||||
|
if not raw_link:
|
||||||
|
return ""
|
||||||
|
if not isinstance(raw_link, str):
|
||||||
|
return str(raw_link)
|
||||||
|
|
||||||
|
if provider == "wfs":
|
||||||
|
url_match = re.search(r"url\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
|
||||||
|
type_match = re.search(r"typename\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
|
||||||
|
if url_match:
|
||||||
|
url = url_match.group(1).strip()
|
||||||
|
if type_match:
|
||||||
|
typename = type_match.group(1).strip()
|
||||||
|
separator = "&" if "?" in url else "?"
|
||||||
|
return f"url={url}{separator}service=WFS&request=GetFeature&typename={typename}"
|
||||||
|
return f"url={url}"
|
||||||
|
|
||||||
|
if provider == "wms":
|
||||||
|
# falls WMS-URL als url='...' vorliegt
|
||||||
|
match = re.search(r"url\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip()
|
||||||
|
|
||||||
|
if provider == "rest":
|
||||||
|
# REST/ArcGIS-Server: direkt nutzen
|
||||||
|
match = re.search(r"url\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip()
|
||||||
|
|
||||||
|
# allgemeines Rückfallverhalten
|
||||||
|
match = re.search(r"url\s*=\s*['\"]([^'\"]+)['\"]", raw_link, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip()
|
||||||
|
return raw_link.strip()
|
||||||
|
|
||||||
|
for row_index, raw_row in enumerate(rows, start=2):
|
||||||
|
if not isinstance(raw_row, Mapping):
|
||||||
|
pe = pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung="Keine gültigen Links in der Excel-Datei gefunden",
|
meldung="Linklisten-Zeile ist nicht als Dictionary formatiert.",
|
||||||
aktion="read_error",
|
aktion="ungueltige_zeile",
|
||||||
kontext=filepath,
|
kontext={"zeile": row_index, "wert": raw_row},
|
||||||
)
|
)
|
||||||
processed_summary = self.pruefmanager.verarbeite(pe_fail)
|
results.append(self.pruefmanager.verarbeite(pe))
|
||||||
return None, processed_summary
|
continue
|
||||||
|
|
||||||
|
normalized = {str(k).strip().lower(): v for k, v in raw_row.items() if k is not None}
|
||||||
|
if not required_keys.issubset(normalized.keys()):
|
||||||
|
missing = required_keys.difference(normalized.keys())
|
||||||
|
pe = pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Linkliste fehlt erforderliche Spalten: {', '.join(sorted(missing))}",
|
||||||
|
aktion="spaltenfehlend",
|
||||||
|
kontext={"zeile": row_index, "fehlend": sorted(missing)},
|
||||||
|
)
|
||||||
|
results.append(self.pruefmanager.verarbeite(pe))
|
||||||
|
continue
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
ident = normalized.get("ident")
|
||||||
# Einzellink-Verarbeitung
|
link_raw = normalized.get("link") or ""
|
||||||
# ------------------------------------------------------------------ #
|
provider = str(normalized.get("provider") or "").strip().lower()
|
||||||
def process_single_link(
|
stildatei_raw = normalized.get("stildatei") or ""
|
||||||
|
stildatei = None
|
||||||
|
|
||||||
|
if stildatei_raw and str(stildatei_raw).strip():
|
||||||
|
style_result = self.stil_pruefer.pruefe(str(stildatei_raw).strip())
|
||||||
|
results.append(self.pruefmanager.verarbeite(style_result))
|
||||||
|
if style_result.ok:
|
||||||
|
# Style-Pfad in der Datenkette beibehalten (absolut, wenn vorhanden).
|
||||||
|
stildatei = str(style_result.kontext or stildatei_raw).strip()
|
||||||
|
else:
|
||||||
|
stildatei = None
|
||||||
|
else:
|
||||||
|
results.append(self.pruefmanager.verarbeite(pruef_ergebnis(ok=True, meldung="Kein Stil angegeben", aktion="stil_optional", kontext=None)))
|
||||||
|
stildatei = None
|
||||||
|
|
||||||
|
if not ident or not link_raw or not provider:
|
||||||
|
pe = pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung="Linklisten-Zeile hat fehlende Pflichtfelder (ident/link/provider).",
|
||||||
|
aktion="pflichtfelder_fehlen",
|
||||||
|
kontext={"zeile": row_index, "daten": raw_row},
|
||||||
|
)
|
||||||
|
results.append(self.pruefmanager.verarbeite(pe))
|
||||||
|
continue
|
||||||
|
|
||||||
|
link_url = extract_url(link_raw, provider)
|
||||||
|
|
||||||
|
# Provider-abhängige Linkvalidierung
|
||||||
|
if provider in ("wfs", "wms", "rest"):
|
||||||
|
# Webdienste: wir akzeptieren die URL-Form und prüfen nicht per network_head.
|
||||||
|
link_result = pruef_ergebnis(ok=True, meldung="Service-Link angenommen", aktion="service_link", kontext=link_url)
|
||||||
|
elif provider in ("ogr", "gpkg", "shp", "geojson"):
|
||||||
|
# OGR/Pfad: mit Linkpruefer (pfad oder lokale Datei) prüfen
|
||||||
|
link_result = self.link_pruefer.pruefe(link_url)
|
||||||
|
else:
|
||||||
|
link_result = self.link_pruefer.pruefe(link_url)
|
||||||
|
|
||||||
|
results.append(self.pruefmanager.verarbeite(link_result))
|
||||||
|
|
||||||
|
# stildatei wurde bereits oben geprüft und ggf. auf Dateiname gesetzt oder auf None
|
||||||
|
|
||||||
|
if not link_result.ok:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Zeile {row_index}: fehlerhafter Link",
|
||||||
|
aktion="link_unvollstaendig",
|
||||||
|
kontext={"row": row_index, "ident": ident},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
result_row = {
|
||||||
|
"ident": ident,
|
||||||
|
"gruppe": normalized.get("gruppe"),
|
||||||
|
"Kartenebene": normalized.get("kartenebene"),
|
||||||
|
"Inhalt": normalized.get("inhalt"),
|
||||||
|
"Link": link_url,
|
||||||
|
"Provider": provider,
|
||||||
|
"stildatei": stildatei,
|
||||||
|
}
|
||||||
|
source_dict["rows"].append(result_row)
|
||||||
|
|
||||||
|
# Validierung über Listenauswerter
|
||||||
|
listenauswerter = Listenauswerter(self.pruefmanager, self.stil_pruefer or Stilpruefer())
|
||||||
|
validated, validation_results = listenauswerter.validate_rows(source_dict)
|
||||||
|
results.extend(validation_results)
|
||||||
|
return validated, results
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Datenbank‑Quellen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _process_database_source(
|
||||||
|
self, db_path: str
|
||||||
|
) -> Tuple[SourceDict, List[pruef_ergebnis]]:
|
||||||
|
print(f"[DataGrabber] Debug: _process_database_source called, db_path={db_path}")
|
||||||
|
source_dict: SourceDict = {}
|
||||||
|
results: List[pruef_ergebnis] = []
|
||||||
|
return source_dict, results
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Dienst‑Quellen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _process_service_source(
|
||||||
|
self, link: str
|
||||||
|
) -> Tuple[SourceDict, List[pruef_ergebnis]]:
|
||||||
|
source_dict: SourceDict = {}
|
||||||
|
results: List[pruef_ergebnis] = []
|
||||||
|
return source_dict, results
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Aggregation
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _aggregate_results(
|
||||||
self,
|
self,
|
||||||
link: Mapping[str, Any]
|
source: str,
|
||||||
) -> Tuple[Optional[Dict[str, List[Mapping[str, Any]]]], Any]:
|
source_dict: SourceDict,
|
||||||
|
partial_results: List[pruef_ergebnis],
|
||||||
|
) -> pruef_ergebnis:
|
||||||
"""
|
"""
|
||||||
Prüft einen einzelnen Link anhand der im Link-Dict enthaltenen Link-URL.
|
Aggregiert Einzelprüfungen zu einem Gesamt-``pruef_ergebnis``.
|
||||||
|
|
||||||
Ablauf
|
**Keine UI-Interaktion.**
|
||||||
------
|
|
||||||
1. Erwartet wird ein Mapping (z.B. dict), das die Linkparameter enthält.
|
|
||||||
Mindestens der Schlüssel ``"Link"`` muss vorhanden sein.
|
|
||||||
|
|
||||||
2. Der eigentliche Link (z.B. URL) wird aus ``link["Link"]`` extrahiert
|
|
||||||
und an ``self.link_pruefer.pruefe(...)`` übergeben.
|
|
||||||
|
|
||||||
3. Das Prüfergebnis wird anschließend durch ``self.pruefmanager.verarbeite(...)``
|
|
||||||
geleitet, der UIInteraktion, Logging und finale Entscheidung übernimmt.
|
|
||||||
|
|
||||||
4. Wenn das verarbeitete Prüfergebnis **nicht OK** ist, wird
|
|
||||||
``(None, pruef_ergebnis)`` zurückgegeben.
|
|
||||||
|
|
||||||
5. Wenn das Prüfergebnis **OK** ist, wird das unveränderte LinkDict
|
|
||||||
in der Struktur ``{"rows": [link]}`` zurückgegeben.
|
|
||||||
|
|
||||||
Parameter
|
|
||||||
---------
|
|
||||||
link:
|
|
||||||
Ein Mapping mit den Linkparametern (z.B. id, Thema, Gruppe, Link,
|
|
||||||
Anbieter, Stildatei). Diese Methode verändert das Mapping nicht.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
Tuple[Optional[Dict[str, List[Mapping[str, Any]]]], pruef_ergebnis]
|
|
||||||
- ``data``: ``{"rows": [link]}`` wenn gültig, sonst ``None``
|
|
||||||
- ``pruef_ergebnis``: das vom Pruefmanager verarbeitete Ergebnis
|
|
||||||
|
|
||||||
Hinweise
|
|
||||||
--------
|
|
||||||
- Diese Methode führt **keine Normalisierung** durch.
|
|
||||||
- Die Verantwortung für die Struktur des Link-Dicts liegt beim Fachplugin.
|
|
||||||
- Der Linkpruefer prüft ausschließlich den Wert ``link["Link"]``.
|
|
||||||
"""
|
"""
|
||||||
|
rows = source_dict.get("rows") if isinstance(source_dict, dict) else None
|
||||||
|
if rows:
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
meldung="Quelle erfolgreich geprüft",
|
||||||
|
aktion="ok",
|
||||||
|
kontext={
|
||||||
|
"source": source,
|
||||||
|
"valid_entries": len(rows),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# 1) Link extrahieren (Fachplugin garantiert, dass "Link" existiert)
|
# Wenn die Linkliste zwar gelesen wurde, aber keine gültigen Zeilen verfügbar sind, geben wir spezifischere Infos zurück.
|
||||||
raw_link = link.get("Link")
|
return pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
# 2) Fachliche Prüfung durch den Linkpruefer
|
meldung="Keine validen Einträge in der Linkliste gefunden",
|
||||||
pruef_ergebnis = self.link_pruefer.pruefe(raw_link)
|
aktion="keine_validen_eintraege",
|
||||||
|
kontext={
|
||||||
# 3) Verarbeitung durch den Pruefmanager
|
"source": source,
|
||||||
processed = self.pruefmanager.verarbeite(pruef_ergebnis)
|
"eintraege_gesamt": len(source_dict.get("rows", [])),
|
||||||
|
},
|
||||||
# 4) Wenn Prüfung nicht OK → keine Daten zurückgeben
|
)
|
||||||
if not getattr(processed, "ok", False):
|
|
||||||
return None, processed
|
|
||||||
|
|
||||||
# 5) Prüfung OK → unverändertes Link-Dict zurückgeben
|
|
||||||
data = {"rows": [link]}
|
|
||||||
|
|
||||||
return data, processed
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# Datenbank-Verarbeitung
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
#def process_database_table(self, db_path: str, table: Optional[str] = None) -> Tuple[Optional[Dict[str, List[Mapping[str, Any]]]], pruef_ergebnis]:
|
|
||||||
#noch nicht implementiert
|
|
||||||
"""
|
|
||||||
Liest eine Tabelle aus einer SQLite/GeoPackage-Datei.
|
|
||||||
|
|
||||||
Verhalten
|
|
||||||
---------
|
|
||||||
1. Validiert die Datei mit dem Dateipruefer.
|
|
||||||
2. Falls OK, versucht es, die angegebene Tabelle zu lesen; falls keine Tabelle
|
|
||||||
angegeben ist, wird nach einer typischen Metadaten-Tabelle 'layer_metadaten'
|
|
||||||
gesucht und diese gelesen.
|
|
||||||
3. Gibt die Zeilen als Liste von Dicts zurück.
|
|
||||||
"""
|
|
||||||
dp = self._datei_pruefer_cls(pfad=db_path, temporaer_erlaubt=False)
|
|
||||||
pe = dp.pruefe()
|
|
||||||
processed = self.pruefmanager.verarbeite(pe)
|
|
||||||
if not getattr(processed, "ok", False):
|
|
||||||
return None, processed
|
|
||||||
|
|
||||||
conn = sqlite3.connect(db_path)
|
|
||||||
cur = conn.cursor()
|
|
||||||
if table:
|
|
||||||
cur.execute(f"SELECT * FROM {table}")
|
|
||||||
cols = [d[0] for d in cur.description]
|
|
||||||
rows = [dict(zip(cols, r)) for r in cur.fetchall()]
|
|
||||||
else:
|
|
||||||
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='layer_metadaten'")
|
|
||||||
if cur.fetchone():
|
|
||||||
cur.execute("SELECT * FROM layer_metadaten")
|
|
||||||
cols = [d[0] for d in cur.description]
|
|
||||||
rows = [dict(zip(cols, r)) for r in cur.fetchall()]
|
|
||||||
else:
|
|
||||||
rows = []
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
pe_ok = pruef_ergebnis(ok=True, meldung="DB gelesen", aktion="ok", kontext=db_path)
|
|
||||||
processed_ok = self.pruefmanager.verarbeite(pe_ok)
|
|
||||||
return {"rows": rows}, processed_ok
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
# Hauptlauf / Dispatch
|
|
||||||
# ------------------------------------------------------------------ #
|
|
||||||
def run(self) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Hauptmethode des DataGrabbers.
|
|
||||||
|
|
||||||
Ablauf
|
|
||||||
------
|
|
||||||
1. Prüft, ob eine Quelle gesetzt ist.
|
|
||||||
2. Klassifiziert die Quelle via :meth:`analyze_source`.
|
|
||||||
3. Dispatch:
|
|
||||||
- file (.xlsx/.xls) -> :meth:`process_excel_source`
|
|
||||||
- link -> :meth:`process_single_link`
|
|
||||||
- database -> :meth:`process_database_table`
|
|
||||||
- unknown -> Fehler
|
|
||||||
4. Aggregiert geladene Einträge in einem Ergebnis-Dict und gibt dieses zurück.
|
|
||||||
|
|
||||||
Rückgabeformat
|
|
||||||
-------------
|
|
||||||
Ein Dict mit den Schlüsseln:
|
|
||||||
- 'geladen' : Liste der geladenen Themen/Namen
|
|
||||||
- 'fehler' : Mapping Thema -> Fehlermeldung
|
|
||||||
- 'ausserhalb': Liste der als ausserhalb klassifizierten Themen
|
|
||||||
- 'relevant' : Liste der relevanten Themen
|
|
||||||
- 'details' : zusätzliche Detailinformationen (z. B. Anzahl Zeilen)
|
|
||||||
"""
|
|
||||||
result: Dict[str, Any] = {"geladen": [], "fehler": {}, "ausserhalb": [], "relevant": [], "details": {}}
|
|
||||||
|
|
||||||
if not self.source:
|
|
||||||
pe = pruef_ergebnis(ok=False, meldung="Keine Quelle gesetzt", aktion="kein_dateipfad", kontext=None)
|
|
||||||
processed = self.pruefmanager.verarbeite(pe)
|
|
||||||
result["fehler"]["source"] = getattr(processed, "meldung", "Keine Quelle")
|
|
||||||
return result
|
|
||||||
|
|
||||||
src_type = self.analyze_source(self.source)
|
|
||||||
|
|
||||||
if src_type == "file":
|
|
||||||
suffix = Path(self.source).suffix.lower()
|
|
||||||
if suffix in (".xlsx", ".xls"):
|
|
||||||
data_dict, pe = self.process_excel_source(self.source)
|
|
||||||
else:
|
|
||||||
pe = pruef_ergebnis(ok=False, meldung="Dateityp nicht unterstützt", aktion="falsche_endung", kontext=self.source)
|
|
||||||
pe = self.pruefmanager.verarbeite(pe)
|
|
||||||
data_dict = None
|
|
||||||
|
|
||||||
elif src_type == "link":
|
|
||||||
data_dict, pe = self.process_single_link(self.source)
|
|
||||||
|
|
||||||
#elif src_type == "database":
|
|
||||||
#data_dict, pe = self.process_database_table(self.source, table=None)
|
|
||||||
|
|
||||||
else:
|
|
||||||
pe = pruef_ergebnis(ok=False, meldung="Quelle unbekannt", aktion="kein_dateipfad", kontext=self.source)
|
|
||||||
pe = self.pruefmanager.verarbeite(pe)
|
|
||||||
data_dict = None
|
|
||||||
|
|
||||||
# Falls Daten vorhanden: fülle result['geladen'] und details
|
|
||||||
if data_dict and "rows" in data_dict:
|
|
||||||
rows = data_dict["rows"]
|
|
||||||
for r in rows:
|
|
||||||
thema = r.get("Inhalt") or r.get("ident") or r.get("Link") or "unbenannt"
|
|
||||||
result["geladen"].append(thema)
|
|
||||||
result["details"]["source_rows"] = len(rows)
|
|
||||||
|
|
||||||
# Falls das letzte pruef_ergebnis einen Fehler enthält, übernehme es
|
|
||||||
if not getattr(pe, "ok", False):
|
|
||||||
result["fehler"]["source"] = getattr(pe, "meldung", "Fehler bei Quelle")
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|||||||
@@ -1,98 +1,215 @@
|
|||||||
"""
|
"""
|
||||||
sn_basis/modules/Dateipruefer.py – Prüfung von Dateieingaben für das Plugin.
|
sn_basis/modules/Dateipruefer.py
|
||||||
Verwendet sys_wrapper und gibt pruef_ergebnis an den Pruefmanager zurück.
|
|
||||||
|
Erweiterter Dateiprüfer für Verfahrens-DB-Workflows mit vollständiger Unterstützung
|
||||||
|
der Anforderungen 1-2.e (leerer Pfad, fehlende Datei, bestehende Datei).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional, Literal
|
||||||
|
|
||||||
from sn_basis.functions.sys_wrapper import (
|
from sn_basis.functions.sys_wrapper import join_path, file_exists
|
||||||
join_path,
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
|
||||||
file_exists,
|
DateiTyp = Literal["excel","datenbank","unbekannt"]
|
||||||
)
|
|
||||||
|
|
||||||
from sn_basis.modules.Pruefmanager import pruef_ergebnis
|
|
||||||
|
|
||||||
|
|
||||||
class Dateipruefer:
|
class Dateipruefer:
|
||||||
"""
|
"""
|
||||||
Prüft Dateieingaben und liefert ein pruef_ergebnis zurück.
|
Prüft Dateieingaben für Verfahrens-DB-Workflows und liefert :class:`pruef_ergebnis`.
|
||||||
Die eigentliche Nutzerinteraktion übernimmt der Pruefmanager.
|
|
||||||
|
**Funktionsweise (deine Anforderungen 1-2.e):**
|
||||||
|
|
||||||
|
+---------------------+------------------------------------------+---------------+
|
||||||
|
| **Fall** | **Ergebnis** | **ok** |
|
||||||
|
+=====================+==========================================+===============+
|
||||||
|
| 1. Leerer Pfad | ``temporaer_erlaubt`` | False |
|
||||||
|
+---------------------+------------------------------------------+---------------+
|
||||||
|
| 2.a Leerer Pfad | Pruefmanager fragt → ``temporaer_erzeugen`` | True |
|
||||||
|
+---------------------+------------------------------------------+---------------+
|
||||||
|
| 2.b Datei existiert | ``ok`` | True |
|
||||||
|
+---------------------+------------------------------------------+---------------+
|
||||||
|
| 2.c Ungültiger Pfad | ``datei_nicht_gefunden`` | False |
|
||||||
|
+---------------------+------------------------------------------+---------------+
|
||||||
|
| **2.d Datei fehlt** | **``datei_wird_erzeugt``** | **True** |
|
||||||
|
+---------------------+------------------------------------------+---------------+
|
||||||
|
| **2.e Datei da** | **``datei_existiert``** | **False** |
|
||||||
|
+---------------------+------------------------------------------+---------------+
|
||||||
|
|
||||||
|
Der Dateiprüfer führt **keine UI-Interaktion** durch.
|
||||||
|
Entscheidungen werden ausschließlich vom :class:`Pruefmanager` getroffen.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
pfad: str,
|
pfad: Optional[str],
|
||||||
basis_pfad: str = "",
|
basis_pfad: str = "",
|
||||||
leereingabe_erlaubt: bool = False,
|
leereingabe_erlaubt: bool = False,
|
||||||
standarddatei: str | None = None,
|
standarddatei: Optional[str] = None,
|
||||||
temporaer_erlaubt: bool = False,
|
temporaer_erlaubt: bool = False,
|
||||||
):
|
*,
|
||||||
|
verfahrens_db_modus: bool = True, # 🆕 Verfahrens-DB-spezifische Logik
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
pfad : Optional[str]
|
||||||
|
Vom UI gelieferter Dateipfad (kann leer oder Whitespace sein).
|
||||||
|
basis_pfad : str, optional
|
||||||
|
Basisverzeichnis für relative Pfade (default: "").
|
||||||
|
leereingabe_erlaubt : bool, optional
|
||||||
|
Ob leere Eingabe grundsätzlich erlaubt ist (default: False).
|
||||||
|
standarddatei : Optional[str], optional
|
||||||
|
Optionaler Standardpfad (default: None).
|
||||||
|
temporaer_erlaubt : bool, optional
|
||||||
|
Ob bei leerer Eingabe temporäre Layer erlaubt sind (default: False).
|
||||||
|
verfahrens_db_modus : bool, optional
|
||||||
|
Aktiviert Verfahrens-DB-spezifische Logik (2.d, 2.e) (default: True).
|
||||||
|
"""
|
||||||
self.pfad = pfad
|
self.pfad = pfad
|
||||||
self.basis_pfad = basis_pfad
|
self.basis_pfad = basis_pfad
|
||||||
self.leereingabe_erlaubt = leereingabe_erlaubt
|
self.leereingabe_erlaubt = leereingabe_erlaubt
|
||||||
self.standarddatei = standarddatei
|
self.standarddatei = standarddatei
|
||||||
self.temporaer_erlaubt = temporaer_erlaubt
|
self.temporaer_erlaubt = temporaer_erlaubt
|
||||||
|
self.verfahrens_db_modus = verfahrens_db_modus
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Hilfsfunktion
|
# Hilfsfunktionen
|
||||||
# ---------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
def erkenne_dateityp(self, pfad: Path) -> DateiTyp:
|
||||||
|
"""
|
||||||
|
Erkennt den Dateityp anhand der Endung.
|
||||||
|
"""
|
||||||
|
suffix = pfad.suffix.lower()
|
||||||
|
|
||||||
|
if suffix == ".xlsx":
|
||||||
|
return "excel"
|
||||||
|
|
||||||
|
if suffix in (".gpkg", ".sqlite"):
|
||||||
|
return "datenbank"
|
||||||
|
|
||||||
|
return "unbekannt"
|
||||||
|
|
||||||
def _pfad(self, relativer_pfad: str) -> Path:
|
def _pfad(self, relativer_pfad: str) -> Path:
|
||||||
"""
|
"""Erzeugt OS-unabhängigen Pfad relativ zum Basisverzeichnis."""
|
||||||
Erzeugt einen OS-unabhängigen Pfad relativ zum Basisverzeichnis.
|
|
||||||
"""
|
|
||||||
return join_path(self.basis_pfad, relativer_pfad)
|
return join_path(self.basis_pfad, relativer_pfad)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Hauptfunktion
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
|
def _ist_leer(self) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft robust, ob Eingabe als „leer" zu behandeln ist.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
True bei None, leerem String oder reinem Whitespace.
|
||||||
|
"""
|
||||||
|
if self.pfad is None:
|
||||||
|
return True
|
||||||
|
if not isinstance(self.pfad, str):
|
||||||
|
return True
|
||||||
|
return not self.pfad.strip()
|
||||||
|
|
||||||
|
def _ist_gueltiger_gpkg_pfad(self, pfad: Path) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft, ob Pfad für GPKG geeignet ist (Endung + Schreibrechte).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
True wenn `.gpkg`-Endung und Verzeichnis beschreibbar.
|
||||||
|
"""
|
||||||
|
if not str(pfad).lower().endswith('.gpkg'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Verzeichnis muss beschreibbar sein
|
||||||
|
return pfad.parent.exists() and pfad.parent.is_dir()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Hauptlogik: deine Anforderungen 1-2.e
|
||||||
|
# ------------------------------------------------------------------
|
||||||
def pruefe(self) -> pruef_ergebnis:
|
def pruefe(self) -> pruef_ergebnis:
|
||||||
"""
|
"""
|
||||||
Prüft eine Dateieingabe und liefert ein pruef_ergebnis zurück.
|
🆕 Prüft Dateieingabe gemäß Anforderungen 1-2.e.
|
||||||
Der Pruefmanager entscheidet später, wie der Nutzer gefragt wird.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# -----------------------------------------------------
|
**Workflow:**
|
||||||
# 1. Fall: Eingabe ist leer
|
1. **Leere Eingabe** → ``temporaer_erlaubt`` (Pruefmanager fragt)
|
||||||
# -----------------------------------------------------
|
2. **Pfad prüfen**:
|
||||||
if not self.pfad:
|
- **Ungültig** → 2.c ``datei_nicht_gefunden``
|
||||||
|
- **Gültig, fehlt** → **2.d** ``datei_wird_erzeugt`` (ok=True!)
|
||||||
|
- **Gültig, existiert** → **2.e** ``datei_existiert`` (Pruefmanager fragt)
|
||||||
|
3. **Datei OK** → 2.b ``ok``
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
pruef_ergebnis
|
||||||
|
Mit korrekter Aktion für jeden Fall.
|
||||||
|
"""
|
||||||
|
# 1. 🎯 ANFORDERUNG 1: Leere Eingabe
|
||||||
|
if self._ist_leer():
|
||||||
return self._handle_leere_eingabe()
|
return self._handle_leere_eingabe()
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# 2. Pfad normalisieren
|
||||||
# 2. Fall: Eingabe ist nicht leer → Datei prüfen
|
pfad = self._pfad(self.pfad.strip())
|
||||||
# -----------------------------------------------------
|
#Excel-dateien erkennen
|
||||||
pfad = self._pfad(self.pfad)
|
dateityp = self.erkenne_dateityp(pfad)
|
||||||
|
|
||||||
|
if dateityp == "excel":
|
||||||
if not file_exists(pfad):
|
if not file_exists(pfad):
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung=f"Die Datei '{self.pfad}' wurde nicht gefunden.",
|
meldung=f"Excel-Datei '{self.pfad}' wurde nicht gefunden.",
|
||||||
aktion="datei_nicht_gefunden",
|
aktion="datei_nicht_gefunden",
|
||||||
kontext=pfad,
|
kontext=pfad,
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------
|
|
||||||
# 3. Datei existiert → Erfolg
|
|
||||||
# -----------------------------------------------------
|
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=True,
|
ok=True,
|
||||||
meldung="Datei gefunden.",
|
meldung="Excel-Datei ist gültig.",
|
||||||
aktion="ok",
|
aktion="ok",
|
||||||
kontext=pfad,
|
kontext=pfad,
|
||||||
)
|
)
|
||||||
|
if dateityp != "datenbank":
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Der Pfad '{self.pfad}' ist kein unterstützter Dateityp.",
|
||||||
|
aktion="unbekannter_dateityp",
|
||||||
|
kontext=pfad,
|
||||||
|
)
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# 🆕 2.c: Ungültiger GPKG-Pfad?
|
||||||
# Behandlung leerer Eingaben
|
if not self.verfahrens_db_modus or not self._ist_gueltiger_gpkg_pfad(pfad):
|
||||||
# ---------------------------------------------------------
|
return pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Der Pfad '{self.pfad}' ist kein gültiger GPKG-Pfad.",
|
||||||
|
aktion="datei_nicht_gefunden",
|
||||||
|
kontext=pfad,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 🆕 2.d: Gültiger Pfad, Datei fehlt → DIREKT WEITER (ok=True!)
|
||||||
|
if not file_exists(pfad):
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=True, # 🎯 WICHTIG: Pipeline fortsetzen!
|
||||||
|
meldung=f"Datei '{self.pfad}' wird erzeugt.",
|
||||||
|
aktion="datei_wird_erzeugt",
|
||||||
|
kontext=pfad,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 🆕 2.e: Datei existiert → Pruefmanager fragt Überschreiben/etc.
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=False, # 🎯 Pruefmanager soll 4-Optionen-Dialog zeigen
|
||||||
|
meldung=f"Datei '{self.pfad}' existiert bereits.",
|
||||||
|
aktion="datei_existiert",
|
||||||
|
kontext=pfad,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2.b: Wird nicht erreicht (durch 2.e abgefangen)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Leere Eingabe (ANFORDERUNG 1, 2.a)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
def _handle_leere_eingabe(self) -> pruef_ergebnis:
|
def _handle_leere_eingabe(self) -> pruef_ergebnis:
|
||||||
"""
|
"""
|
||||||
Liefert ein pruef_ergebnis für den Fall, dass das Dateifeld leer ist.
|
Behandelt leere Eingaben (Priorität: leereingabe → Standard → temporär → Fehler).
|
||||||
Der Pruefmanager fragt später den Nutzer.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 1. Leereingabe erlaubt → Nutzer fragen, ob das beabsichtigt war
|
|
||||||
if self.leereingabe_erlaubt:
|
if self.leereingabe_erlaubt:
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
@@ -101,19 +218,17 @@ class Dateipruefer:
|
|||||||
kontext=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Standarddatei verfügbar → Nutzer fragen, ob sie verwendet werden soll
|
|
||||||
if self.standarddatei:
|
if self.standarddatei:
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung=(
|
meldung=(
|
||||||
f"Es wurde keine Datei angegeben. "
|
"Es wurde keine Datei angegeben. "
|
||||||
f"Soll die Standarddatei '{self.standarddatei}' verwendet werden?"
|
f"Soll die Standarddatei '{self.standarddatei}' verwendet werden?"
|
||||||
),
|
),
|
||||||
aktion="standarddatei_vorschlagen",
|
aktion="standarddatei_vorschlagen",
|
||||||
kontext=self._pfad(self.standarddatei),
|
kontext=self._pfad(self.standarddatei),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. Temporäre Datei erlaubt → Nutzer fragen, ob temporär gearbeitet werden soll
|
|
||||||
if self.temporaer_erlaubt:
|
if self.temporaer_erlaubt:
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
@@ -125,7 +240,6 @@ class Dateipruefer:
|
|||||||
kontext=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 4. Leereingabe nicht erlaubt → Fehler
|
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung="Es wurde keine Datei angegeben.",
|
meldung="Es wurde keine Datei angegeben.",
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ Designprinzipien
|
|||||||
- Die Methode ist pdoc-kompatibel dokumentiert und bewusst einfach gehalten.
|
- Die Methode ist pdoc-kompatibel dokumentiert und bewusst einfach gehalten.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict, List, Mapping, Optional, Tuple
|
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple
|
||||||
|
|
||||||
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse
|
from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
from sn_basis.functions import qgiscore_wrapper as qgiscore
|
from sn_basis.functions import qgiscore_wrapper as qgiscore
|
||||||
@@ -59,6 +60,7 @@ class Datenabruf:
|
|||||||
verfahrensgebiet_layer: Any,
|
verfahrensgebiet_layer: Any,
|
||||||
speicherort: str,
|
speicherort: str,
|
||||||
pruef_ergebnisse: Optional[List[Any]] = None,
|
pruef_ergebnisse: Optional[List[Any]] = None,
|
||||||
|
progress: Optional[Any] = None,
|
||||||
) -> Tuple[Dict[str, Any], List[Any]]:
|
) -> Tuple[Dict[str, Any], List[Any]]:
|
||||||
"""
|
"""
|
||||||
Ruft für alle Zeilen in ``result_dict["rows"]`` die Fachdaten ab und
|
Ruft für alle Zeilen in ``result_dict["rows"]`` die Fachdaten ab und
|
||||||
@@ -82,6 +84,10 @@ class Datenabruf:
|
|||||||
|
|
||||||
# 1) Räumliche Filtergeometrie bestimmen (BBox oder None)
|
# 1) Räumliche Filtergeometrie bestimmen (BBox oder None)
|
||||||
bbox_geom = self._determine_spatial_filter(raumfilter, verfahrensgebiet_layer)
|
bbox_geom = self._determine_spatial_filter(raumfilter, verfahrensgebiet_layer)
|
||||||
|
filter_crs_authid = None
|
||||||
|
if isinstance(bbox_geom, dict):
|
||||||
|
raw_crs = bbox_geom.get("crs_authid")
|
||||||
|
filter_crs_authid = str(raw_crs) if raw_crs else None
|
||||||
|
|
||||||
# Globale Logs über alle Dienste hinweg
|
# Globale Logs über alle Dienste hinweg
|
||||||
log_geladen: Dict[str, int] = {}
|
log_geladen: Dict[str, int] = {}
|
||||||
@@ -90,7 +96,20 @@ class Datenabruf:
|
|||||||
log_ausserhalb: Dict[str, int] = {}
|
log_ausserhalb: Dict[str, int] = {}
|
||||||
|
|
||||||
# 2) Über alle Zeilen iterieren
|
# 2) Über alle Zeilen iterieren
|
||||||
for row in rows:
|
total_rows = len(rows)
|
||||||
|
for idx, row in enumerate(rows, start=1):
|
||||||
|
if progress is not None:
|
||||||
|
progress.set_label(f"Datenabruf {idx}/{total_rows}…")
|
||||||
|
if progress.is_canceled():
|
||||||
|
pe_cancel = pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung="Datenabruf durch Benutzer abgebrochen",
|
||||||
|
aktion="abbruch",
|
||||||
|
kontext={"schritt": idx},
|
||||||
|
)
|
||||||
|
processed_results.append(self.pruefmanager.verarbeite(pe_cancel))
|
||||||
|
break
|
||||||
|
|
||||||
ident = row.get("ident")
|
ident = row.get("ident")
|
||||||
link = row.get("Link")
|
link = row.get("Link")
|
||||||
provider = row.get("Provider")
|
provider = row.get("Provider")
|
||||||
@@ -115,7 +134,16 @@ class Datenabruf:
|
|||||||
url = self._build_provider_url(link=link, provider=str(provider), bbox_geom=bbox_geom if use_bbox else None)
|
url = self._build_provider_url(link=link, provider=str(provider), bbox_geom=bbox_geom if use_bbox else None)
|
||||||
|
|
||||||
# 2b) Fachdaten abrufen
|
# 2b) Fachdaten abrufen
|
||||||
features, error_msg = self._fetch_features(url=url, provider=str(provider))
|
features, error_msg = self._fetch_features(
|
||||||
|
url=url,
|
||||||
|
provider=str(provider),
|
||||||
|
cancel_callback=(progress.is_canceled if progress is not None else None),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if progress is not None:
|
||||||
|
if hasattr(progress, "set_value"):
|
||||||
|
progress.set_value(idx)
|
||||||
|
|
||||||
# 2c) Logs und Aggregation
|
# 2c) Logs und Aggregation
|
||||||
if error_msg:
|
if error_msg:
|
||||||
@@ -207,7 +235,18 @@ class Datenabruf:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if raumfilter == "Verfahrensgebiet":
|
if raumfilter == "Verfahrensgebiet":
|
||||||
return qgiscore.get_layer_extent(verfahrensgebiet_layer)
|
extent = qgiscore.get_layer_extent(verfahrensgebiet_layer)
|
||||||
|
if extent is None:
|
||||||
|
return None
|
||||||
|
crs_authid = None
|
||||||
|
try:
|
||||||
|
if hasattr(verfahrensgebiet_layer, "crs") and callable(getattr(verfahrensgebiet_layer, "crs")):
|
||||||
|
crs = verfahrensgebiet_layer.crs()
|
||||||
|
if crs is not None and hasattr(crs, "authid") and callable(getattr(crs, "authid")):
|
||||||
|
crs_authid = crs.authid()
|
||||||
|
except Exception:
|
||||||
|
crs_authid = None
|
||||||
|
return {"extent": extent, "crs_authid": crs_authid}
|
||||||
|
|
||||||
if raumfilter == "Pufferlayer":
|
if raumfilter == "Pufferlayer":
|
||||||
buffer_layer = qgiscore.create_buffer_layer(
|
buffer_layer = qgiscore.create_buffer_layer(
|
||||||
@@ -216,8 +255,18 @@ class Datenabruf:
|
|||||||
layer_name="Verfahrensgebiet_Puffer_1km",
|
layer_name="Verfahrensgebiet_Puffer_1km",
|
||||||
)
|
)
|
||||||
if buffer_layer is not None:
|
if buffer_layer is not None:
|
||||||
qgisui.add_layer_to_project(buffer_layer)
|
extent = qgiscore.get_layer_extent(buffer_layer)
|
||||||
return qgiscore.get_layer_extent(buffer_layer)
|
if extent is None:
|
||||||
|
return None
|
||||||
|
crs_authid = None
|
||||||
|
try:
|
||||||
|
if hasattr(buffer_layer, "crs") and callable(getattr(buffer_layer, "crs")):
|
||||||
|
crs = buffer_layer.crs()
|
||||||
|
if crs is not None and hasattr(crs, "authid") and callable(getattr(crs, "authid")):
|
||||||
|
crs_authid = crs.authid()
|
||||||
|
except Exception:
|
||||||
|
crs_authid = None
|
||||||
|
return {"extent": extent, "crs_authid": crs_authid}
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -233,60 +282,130 @@ class Datenabruf:
|
|||||||
Erwartet: provider ist gesetzt (z. B. "WFS", "REST", "OGR", "WMS").
|
Erwartet: provider ist gesetzt (z. B. "WFS", "REST", "OGR", "WMS").
|
||||||
"""
|
"""
|
||||||
provider_norm = (provider or "").upper()
|
provider_norm = (provider or "").upper()
|
||||||
base_link = link or ""
|
base_link = (link or "").strip()
|
||||||
|
if base_link.lower().startswith("url="):
|
||||||
|
base_link = base_link[4:].strip()
|
||||||
|
|
||||||
# WMS: niemals BBOX anhängen
|
if provider_norm == "WFS" and base_link.count("?") > 1:
|
||||||
|
first, rest = base_link.split("?", 1)
|
||||||
|
base_link = f"{first}?{rest.replace('?', '&')}"
|
||||||
|
|
||||||
|
extent_obj = bbox_geom
|
||||||
|
crs_authid: Optional[str] = None
|
||||||
|
if isinstance(bbox_geom, dict):
|
||||||
|
extent_obj = bbox_geom.get("extent")
|
||||||
|
raw_crs = bbox_geom.get("crs_authid")
|
||||||
|
crs_authid = str(raw_crs) if raw_crs else None
|
||||||
|
|
||||||
|
# WMS: unverändert durchreichen
|
||||||
if provider_norm == "WMS":
|
if provider_norm == "WMS":
|
||||||
return base_link
|
return base_link
|
||||||
|
|
||||||
if bbox_geom is None:
|
# Versuche bbox-String zu erzeugen (falls Raumfilter aktiv)
|
||||||
return base_link
|
|
||||||
|
|
||||||
# Versuche bbox-String zu erzeugen (nutzt qgiscore.extent_to_bbox_string wenn vorhanden)
|
|
||||||
bbox_str: Optional[str] = None
|
bbox_str: Optional[str] = None
|
||||||
|
if extent_obj is not None:
|
||||||
try:
|
try:
|
||||||
extent_to_bbox = getattr(__import__("sn_basis.functions.qgiscore_wrapper", fromlist=["qgiscore_wrapper"]), "extent_to_bbox_string", None)
|
extent_to_bbox = getattr(__import__("sn_basis.functions.qgiscore_wrapper", fromlist=["qgiscore_wrapper"]), "extent_to_bbox_string", None)
|
||||||
if callable(extent_to_bbox):
|
if callable(extent_to_bbox):
|
||||||
bbox_str = extent_to_bbox(bbox_geom)
|
bbox_str = extent_to_bbox(extent_obj)
|
||||||
else:
|
else:
|
||||||
# Fallback: einfache xmin/ymin/xmax/ymax-Extraktion (duck-typing)
|
# Fallback: einfache xmin/ymin/xmax/ymax-Extraktion (duck-typing)
|
||||||
if hasattr(bbox_geom, "xmin") and callable(getattr(bbox_geom, "xmin")):
|
if hasattr(extent_obj, "xmin") and callable(getattr(extent_obj, "xmin")):
|
||||||
bbox_str = f"{bbox_geom.xmin()},{bbox_geom.ymin()},{bbox_geom.xmax()},{bbox_geom.ymax()}"
|
bbox_str = f"{extent_obj.xmin()},{extent_obj.ymin()},{extent_obj.xmax()},{extent_obj.ymax()}"
|
||||||
elif isinstance(bbox_geom, (tuple, list)) and len(bbox_geom) == 4:
|
elif isinstance(extent_obj, (tuple, list)) and len(extent_obj) == 4:
|
||||||
bbox_str = f"{bbox_geom[0]},{bbox_geom[1]},{bbox_geom[2]},{bbox_geom[3]}"
|
bbox_str = f"{extent_obj[0]},{extent_obj[1]},{extent_obj[2]},{extent_obj[3]}"
|
||||||
else:
|
else:
|
||||||
bbox_str = str(bbox_geom)
|
bbox_str = str(extent_obj)
|
||||||
except Exception:
|
except Exception:
|
||||||
bbox_str = None
|
bbox_str = None
|
||||||
|
|
||||||
if not bbox_str:
|
|
||||||
return base_link
|
|
||||||
|
|
||||||
parsed = urlparse(base_link)
|
parsed = urlparse(base_link)
|
||||||
query_params = dict(parse_qsl(parsed.query, keep_blank_values=True))
|
query_params = dict(parse_qsl(parsed.query, keep_blank_values=True))
|
||||||
|
|
||||||
if provider_norm == "WFS":
|
if provider_norm == "WFS":
|
||||||
|
query_params.setdefault("service", "WFS")
|
||||||
|
query_params.setdefault("request", "GetFeature")
|
||||||
|
query_params.setdefault("outputFormat", "application/json")
|
||||||
|
if bbox_str:
|
||||||
query_params.setdefault("BBOX", bbox_str)
|
query_params.setdefault("BBOX", bbox_str)
|
||||||
|
if crs_authid:
|
||||||
|
query_params.setdefault("SRSNAME", crs_authid)
|
||||||
new_query = urlencode(query_params, doseq=True)
|
new_query = urlencode(query_params, doseq=True)
|
||||||
rebuilt = parsed._replace(query=new_query)
|
rebuilt = parsed._replace(query=new_query)
|
||||||
return urlunparse(rebuilt)
|
return urlunparse(rebuilt)
|
||||||
|
|
||||||
if provider_norm in ("REST", "ARCGIS", "ARCGISFEATURESERVER", "ARCGIS_FEATURESERVER"):
|
if provider_norm in ("REST", "ARCGIS", "ARCGISFEATURESERVER", "ARCGIS_FEATURESERVER"):
|
||||||
|
# ArcGIS FeatureServer erwartet i.d.R. den /query-Endpunkt
|
||||||
|
rest_base = base_link.rstrip("/")
|
||||||
|
if not rest_base.lower().endswith("/query"):
|
||||||
|
rest_base = f"{rest_base}/query"
|
||||||
|
|
||||||
|
parsed_rest = urlparse(rest_base)
|
||||||
|
query_params = dict(parse_qsl(parsed_rest.query, keep_blank_values=True))
|
||||||
|
query_params.setdefault("where", "1=1")
|
||||||
|
query_params.setdefault("outFields", "*")
|
||||||
|
query_params.setdefault("returnGeometry", "true")
|
||||||
|
query_params.setdefault("f", query_params.get("f", "json"))
|
||||||
|
|
||||||
|
if bbox_str:
|
||||||
|
geometry_envelope = None
|
||||||
|
try:
|
||||||
|
if hasattr(extent_obj, "xmin") and callable(getattr(extent_obj, "xmin")):
|
||||||
|
geometry_envelope = {
|
||||||
|
"xmin": extent_obj.xmin(),
|
||||||
|
"ymin": extent_obj.ymin(),
|
||||||
|
"xmax": extent_obj.xmax(),
|
||||||
|
"ymax": extent_obj.ymax(),
|
||||||
|
}
|
||||||
|
elif isinstance(extent_obj, (tuple, list)) and len(extent_obj) == 4:
|
||||||
|
geometry_envelope = {
|
||||||
|
"xmin": extent_obj[0],
|
||||||
|
"ymin": extent_obj[1],
|
||||||
|
"xmax": extent_obj[2],
|
||||||
|
"ymax": extent_obj[3],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
parts = [p.strip() for p in str(bbox_str).split(",")]
|
||||||
|
if len(parts) == 4:
|
||||||
|
geometry_envelope = {
|
||||||
|
"xmin": float(parts[0]),
|
||||||
|
"ymin": float(parts[1]),
|
||||||
|
"xmax": float(parts[2]),
|
||||||
|
"ymax": float(parts[3]),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
geometry_envelope = None
|
||||||
|
|
||||||
|
if geometry_envelope is not None:
|
||||||
|
query_params.setdefault("geometry", json.dumps(geometry_envelope))
|
||||||
|
else:
|
||||||
query_params.setdefault("geometry", bbox_str)
|
query_params.setdefault("geometry", bbox_str)
|
||||||
query_params.setdefault("geometryType", "esriGeometryEnvelope")
|
query_params.setdefault("geometryType", "esriGeometryEnvelope")
|
||||||
query_params.setdefault("spatialRel", "esriSpatialRelIntersects")
|
query_params.setdefault("spatialRel", "esriSpatialRelIntersects")
|
||||||
query_params.setdefault("f", query_params.get("f", "json"))
|
|
||||||
|
if crs_authid and ":" in crs_authid:
|
||||||
|
srid = crs_authid.split(":", 1)[1]
|
||||||
|
if srid.isdigit():
|
||||||
|
query_params.setdefault("inSR", srid)
|
||||||
|
query_params.setdefault("outSR", srid)
|
||||||
|
|
||||||
new_query = urlencode(query_params, doseq=True)
|
new_query = urlencode(query_params, doseq=True)
|
||||||
rebuilt = parsed._replace(query=new_query)
|
rebuilt = parsed_rest._replace(query=new_query)
|
||||||
return urlunparse(rebuilt)
|
return urlunparse(rebuilt)
|
||||||
|
|
||||||
# Default: generischer bbox-Parameter
|
# Default: generischer bbox-Parameter (nur wenn vorhanden)
|
||||||
|
if bbox_str:
|
||||||
query_params.setdefault("bbox", bbox_str)
|
query_params.setdefault("bbox", bbox_str)
|
||||||
new_query = urlencode(query_params, doseq=True)
|
new_query = urlencode(query_params, doseq=True)
|
||||||
rebuilt = parsed._replace(query=new_query)
|
rebuilt = parsed._replace(query=new_query)
|
||||||
return urlunparse(rebuilt)
|
return urlunparse(rebuilt)
|
||||||
|
|
||||||
def _fetch_features(self, url: str, provider: str) -> Tuple[List[Any], Optional[str]]:
|
def _fetch_features(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
provider: str,
|
||||||
|
cancel_callback: Optional[Callable[[], bool]] = None,
|
||||||
|
) -> Tuple[List[Any], Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Führt den eigentlichen Abruf der Fachdaten durch.
|
Führt den eigentlichen Abruf der Fachdaten durch.
|
||||||
|
|
||||||
@@ -336,19 +455,85 @@ class Datenabruf:
|
|||||||
http_error: Optional[str] = None
|
http_error: Optional[str] = None
|
||||||
|
|
||||||
# QGIS NetworkAccessManager bevorzugen
|
# QGIS NetworkAccessManager bevorzugen
|
||||||
|
_FETCH_TIMEOUT_MS = 30_000 # 30 Sekunden
|
||||||
|
aborted_or_timed_out = False
|
||||||
|
attempted_qgis_fetch = False
|
||||||
|
|
||||||
|
if callable(cancel_callback) and cancel_callback():
|
||||||
|
return [], "Abbruch durch Benutzer"
|
||||||
|
|
||||||
if getattr(qgiscore, "QGIS_AVAILABLE", False) and getattr(qgiscore, "QgsNetworkAccessManager", None) is not None:
|
if getattr(qgiscore, "QGIS_AVAILABLE", False) and getattr(qgiscore, "QgsNetworkAccessManager", None) is not None:
|
||||||
|
attempted_qgis_fetch = True
|
||||||
try:
|
try:
|
||||||
manager = qgiscore.QgsNetworkAccessManager.instance()
|
manager = qgiscore.QgsNetworkAccessManager.instance()
|
||||||
QUrl = getattr(__import__("sn_basis.functions.qt_wrapper", fromlist=["qt_wrapper"]), "QUrl", None)
|
# Netzwerk-Timeout global setzen (QGIS >= 3.6)
|
||||||
QNetworkRequest = getattr(__import__("sn_basis.functions.qt_wrapper", fromlist=["qt_wrapper"]), "QNetworkRequest", None)
|
if hasattr(manager, "setTimeout"):
|
||||||
QEventLoop = getattr(__import__("sn_basis.functions.qt_wrapper", fromlist=["qt_wrapper"]), "QEventLoop", None)
|
manager.setTimeout(_FETCH_TIMEOUT_MS)
|
||||||
|
_qt = __import__("sn_basis.functions.qt_wrapper", fromlist=["qt_wrapper"])
|
||||||
|
QUrl = getattr(_qt, "QUrl", None)
|
||||||
|
QNetworkRequest = getattr(_qt, "QNetworkRequest", None)
|
||||||
|
QEventLoop = getattr(_qt, "QEventLoop", None)
|
||||||
|
QTimer = getattr(_qt, "QTimer", None)
|
||||||
if QUrl is not None and QNetworkRequest is not None:
|
if QUrl is not None and QNetworkRequest is not None:
|
||||||
req = QNetworkRequest(QUrl(url))
|
req = QNetworkRequest(QUrl(url))
|
||||||
reply = manager.get(req)
|
reply = manager.get(req)
|
||||||
if QEventLoop is not None:
|
if QEventLoop is not None:
|
||||||
loop = QEventLoop()
|
loop = QEventLoop()
|
||||||
reply.finished.connect(loop.quit)
|
reply.finished.connect(loop.quit)
|
||||||
|
_poll_timer = None
|
||||||
|
if QTimer is not None:
|
||||||
|
try:
|
||||||
|
_poll_timer = QTimer()
|
||||||
|
_poll_timer.setSingleShot(False)
|
||||||
|
_poll_timer.timeout.connect(loop.quit)
|
||||||
|
_poll_timer.start(100)
|
||||||
|
except Exception:
|
||||||
|
_poll_timer = None
|
||||||
|
|
||||||
|
start_time = time.monotonic()
|
||||||
|
while True:
|
||||||
|
if callable(cancel_callback) and cancel_callback():
|
||||||
|
reply.abort()
|
||||||
|
http_error = "Abbruch durch Benutzer"
|
||||||
|
aborted_or_timed_out = True
|
||||||
|
break
|
||||||
|
|
||||||
|
elapsed_ms = int((time.monotonic() - start_time) * 1000)
|
||||||
|
if elapsed_ms >= _FETCH_TIMEOUT_MS:
|
||||||
|
reply.abort()
|
||||||
|
http_error = f"Timeout nach {_FETCH_TIMEOUT_MS // 1000} s: {url}"
|
||||||
|
aborted_or_timed_out = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if hasattr(reply, "isFinished") and reply.isFinished():
|
||||||
|
break
|
||||||
|
|
||||||
loop.exec()
|
loop.exec()
|
||||||
|
try:
|
||||||
|
if hasattr(qt, "QCoreApplication") and hasattr(qt.QCoreApplication, "processEvents"):
|
||||||
|
qt.QCoreApplication.processEvents()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if _poll_timer is not None:
|
||||||
|
try:
|
||||||
|
_poll_timer.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not aborted_or_timed_out:
|
||||||
|
# Fehler aus Reply auslesen
|
||||||
|
err_code = None
|
||||||
|
try:
|
||||||
|
err_code = reply.error()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if err_code and int(err_code) != 0:
|
||||||
|
http_error = f"Netzwerkfehler ({err_code}): {reply.errorString()}"
|
||||||
|
if http_error:
|
||||||
|
# Timeout oder Netzwerkfehler – keinen Body lesen
|
||||||
|
pass
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
raw = reply.readAll()
|
raw = reply.readAll()
|
||||||
data_bytes = bytes(raw) if hasattr(raw, "__bytes__") else raw
|
data_bytes = bytes(raw) if hasattr(raw, "__bytes__") else raw
|
||||||
@@ -362,8 +547,8 @@ class Datenabruf:
|
|||||||
http_error = f"QgsNetworkAccessManager error: {exc}"
|
http_error = f"QgsNetworkAccessManager error: {exc}"
|
||||||
response_text = None
|
response_text = None
|
||||||
|
|
||||||
# Fallback: requests
|
# Fallback: requests nur wenn kein harter Abbruch/Timeout im QGIS-Request vorlag
|
||||||
if response_text is None:
|
if response_text is None and (not attempted_qgis_fetch or not aborted_or_timed_out):
|
||||||
try:
|
try:
|
||||||
import requests # lokal import, keine harte Abhängigkeit
|
import requests # lokal import, keine harte Abhängigkeit
|
||||||
r = requests.get(url, timeout=30)
|
r = requests.get(url, timeout=30)
|
||||||
@@ -383,6 +568,8 @@ class Datenabruf:
|
|||||||
return parsed.get("features", []), None
|
return parsed.get("features", []), None
|
||||||
if isinstance(parsed, dict) and "features" in parsed:
|
if isinstance(parsed, dict) and "features" in parsed:
|
||||||
return parsed.get("features", []), None
|
return parsed.get("features", []), None
|
||||||
|
if prov in ("REST", "ARCGIS", "ARCGISFEATURESERVER", "ARCGIS_FEATURESERVER", "WFS"):
|
||||||
|
return [], "Antwort enthält keine Feature-Liste"
|
||||||
# Sonst: gib das gesamte JSON als einzelnes Objekt zurück
|
# Sonst: gib das gesamte JSON als einzelnes Objekt zurück
|
||||||
return [parsed], None
|
return [parsed], None
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
|
|||||||
@@ -30,9 +30,13 @@ from __future__ import annotations
|
|||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import datetime
|
import datetime
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
from sn_basis.functions import qgiscore_wrapper as qgiscore
|
from sn_basis.functions import qgiscore_wrapper as qgiscore
|
||||||
|
from sn_basis.functions.os_wrapper import normalize_path, is_absolute_path
|
||||||
|
from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
|
||||||
|
|
||||||
@@ -53,10 +57,97 @@ class Datenschreiber:
|
|||||||
|
|
||||||
def __init__(self, pruefmanager: Any, gpkg_path: Optional[str] = None) -> None:
|
def __init__(self, pruefmanager: Any, gpkg_path: Optional[str] = None) -> None:
|
||||||
self.pruefmanager = pruefmanager
|
self.pruefmanager = pruefmanager
|
||||||
self.gpkg_path = gpkg_path
|
self.gpkg_path = str(gpkg_path) if gpkg_path else None
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
def _resolve_style_path(self, style_path: Optional[str]) -> Optional[str]:
|
||||||
# Schreibe Daten
|
if not style_path:
|
||||||
|
return None
|
||||||
|
|
||||||
|
style_path_str = str(style_path).strip()
|
||||||
|
if not style_path_str:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not is_absolute_path(style_path_str):
|
||||||
|
plugin_root = get_plugin_root()
|
||||||
|
style_path_str = str(join_path(plugin_root, "sn_plan41", "assets", style_path_str))
|
||||||
|
|
||||||
|
style_path_str = str(normalize_path(style_path_str))
|
||||||
|
return style_path_str if file_exists(style_path_str) else None
|
||||||
|
|
||||||
|
def _store_style_in_gpkg(self, layer_name: str, style_path: str, layer: Optional[Any] = None) -> None:
|
||||||
|
"""Stellt sicher, dass der Stil in der layer_styles-Tabelle der GPKG gespeichert wird."""
|
||||||
|
try:
|
||||||
|
with open(style_path, "r", encoding="utf-8") as fh:
|
||||||
|
style_qml = fh.read()
|
||||||
|
|
||||||
|
f_geometry_column = ''
|
||||||
|
if layer is not None:
|
||||||
|
try:
|
||||||
|
if hasattr(layer, 'geometryColumn'):
|
||||||
|
f_geometry_column = str(layer.geometryColumn())
|
||||||
|
elif hasattr(layer, 'dataProvider') and hasattr(layer.dataProvider(), 'geometryColumnName'):
|
||||||
|
f_geometry_column = str(layer.dataProvider().geometryColumnName())
|
||||||
|
except Exception:
|
||||||
|
f_geometry_column = ''
|
||||||
|
|
||||||
|
with sqlite3.connect(self.gpkg_path) as conn:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS layer_styles (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
f_table_catalog TEXT,
|
||||||
|
f_table_schema TEXT,
|
||||||
|
f_table_name TEXT NOT NULL,
|
||||||
|
f_geometry_column TEXT,
|
||||||
|
styleName TEXT,
|
||||||
|
styleQML TEXT,
|
||||||
|
styleSLD TEXT,
|
||||||
|
useAsDefault BOOLEAN,
|
||||||
|
description TEXT,
|
||||||
|
owner TEXT,
|
||||||
|
ui TEXT,
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Das aktuelle QGIS-Style-Verhalten: bestehenden Style für denselben Layer nicht löschen (nur appenden)
|
||||||
|
# Wir wollen aber Default-Style setzen: alte Default-Styles entfernen.
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE layer_styles SET useAsDefault = 0 WHERE f_table_name = ?",
|
||||||
|
(layer_name,),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fülle die bekannten QGIS-Kolonnen
|
||||||
|
style_name = os.path.basename(style_path)
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO layer_styles (f_table_catalog, f_table_schema, f_table_name, f_geometry_column, styleName, styleQML, styleSLD, useAsDefault, description, owner, ui) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
(
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
layer_name,
|
||||||
|
f_geometry_column,
|
||||||
|
style_name,
|
||||||
|
style_qml,
|
||||||
|
None,
|
||||||
|
1,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
except Exception as exc:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Fehler beim Speichern des Layer-Stils in GPKG: {exc}",
|
||||||
|
aktion="style_gpkg_speichern_fehlgeschlagen",
|
||||||
|
kontext={"layer_name": layer_name, "style_path": style_path},
|
||||||
|
)
|
||||||
|
)
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
def schreibe_Daten(
|
def schreibe_Daten(
|
||||||
self,
|
self,
|
||||||
@@ -65,192 +156,93 @@ class Datenschreiber:
|
|||||||
speicherort: str,
|
speicherort: str,
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Schreibt die abgerufenen Daten in die Zieldatenbank/Dateien.
|
Schreibt die übergebenen Layer in die Ziel-GPKG.
|
||||||
|
|
||||||
Ablauf
|
Erwartung:
|
||||||
------
|
- daten_dict["daten"] enthält Einträge der Form:
|
||||||
Für jede Zeile (ident) in ``daten_dict["daten"]``:
|
ident -> {"layer": QgsVectorLayer}
|
||||||
1. Bestimme Ziel-Layername (z. B. Thema oder ident).
|
- self.gpkg_path ist ein str
|
||||||
2. Prüfe, ob ein Layer mit diesem Namen bereits existiert (Wrapper).
|
|
||||||
3. Falls vorhanden, frage den Benutzer (Überschreiben / Anhängen / Abbrechen)
|
|
||||||
über die zentrale Pruefmanager-Methode `ask_overwrite_append_cancel`.
|
|
||||||
4. Führe die gewählte Operation aus oder schreibe den Layer, wenn er noch nicht existiert.
|
|
||||||
5. Schreibe ggf. den Stil in die GPKG und setze ihn als Vorgabe.
|
|
||||||
6. Sammle und gib eine Liste der angelegten/geänderten Layer zurück.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
List[Dict[str, Any]]
|
|
||||||
Liste von Dicts mit Informationen zu jedem angelegten/geänderten Layer.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not speicherort:
|
if not speicherort:
|
||||||
raise ValueError("Ein gültiger Speicherort (speicherort) muss übergeben werden.")
|
raise ValueError("Ein gültiger Speicherort (speicherort) muss übergeben werden.")
|
||||||
|
|
||||||
# Setze gpkg_path falls noch nicht vorhanden
|
# gpkg_path einmalig setzen / normalisieren
|
||||||
if not self.gpkg_path:
|
if not self.gpkg_path:
|
||||||
self.gpkg_path = speicherort
|
self.gpkg_path = str(speicherort)
|
||||||
|
|
||||||
results: List[Dict[str, Any]] = []
|
results: List[Dict[str, Any]] = []
|
||||||
daten_map: Dict[str, List[Any]] = daten_dict.get("daten", {})
|
daten_map: Dict[str, Any] = daten_dict.get("daten", {})
|
||||||
|
|
||||||
# Iteriere über alle Einträge
|
for ident, entry in daten_map.items():
|
||||||
for ident, features in daten_map.items():
|
layer = None
|
||||||
# Thema/Name ableiten (falls vorhanden in processed_results oder ident)
|
style_path = None
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Layer extrahieren
|
||||||
|
# -----------------------------
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
layer = entry.get("layer")
|
||||||
|
style_path = self._resolve_style_path(entry.get("style_path"))
|
||||||
|
|
||||||
|
if layer is None or not hasattr(layer, "isValid") or not layer.isValid():
|
||||||
|
pe_err = pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Ungültiger Layer für {ident}",
|
||||||
|
aktion="save_exception",
|
||||||
|
kontext={"ident": ident},
|
||||||
|
)
|
||||||
|
self.pruefmanager.verarbeite(pe_err)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Layername bestimmen
|
||||||
|
# -----------------------------
|
||||||
thema = None
|
thema = None
|
||||||
for pe in processed_results:
|
for pe in processed_results:
|
||||||
try:
|
try:
|
||||||
kontext = getattr(pe, "kontext", None) or {}
|
kontext = getattr(pe, "kontext", None) or {}
|
||||||
if kontext and kontext.get("ident") == ident:
|
if kontext.get("ident") == ident:
|
||||||
thema = kontext.get("thema")
|
thema = kontext.get("thema")
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
if not thema:
|
|
||||||
thema = str(ident)
|
|
||||||
|
|
||||||
layer_name = thema
|
layer_name_raw = thema or str(ident)
|
||||||
|
layer_name = re.sub(r"[^A-Za-z0-9_]+", "_", layer_name_raw).strip("_")
|
||||||
|
if not layer_name:
|
||||||
|
layer_name = f"layer_{ident}"
|
||||||
|
|
||||||
# Prüfe, ob Layer bereits existiert in der Ziel-GPKG
|
# Layer in GPKG schreiben
|
||||||
layer_exists = False
|
err_msg = self._write_layer_to_gpkg(layer_name=layer_name, layer=layer)
|
||||||
try:
|
if err_msg is not None:
|
||||||
layer_exists_fn = getattr(qgiscore, "layer_exists_in_gpkg", None)
|
|
||||||
if callable(layer_exists_fn):
|
|
||||||
layer_exists = layer_exists_fn(self.gpkg_path, layer_name)
|
|
||||||
else:
|
|
||||||
# Fallback: QGIS-Fallback-Check via QgsVectorLayer
|
|
||||||
if getattr(qgiscore, "QgsVectorLayer", None) is not None and qgiscore.QGIS_AVAILABLE:
|
|
||||||
uri = f"{self.gpkg_path}|layername={layer_name}"
|
|
||||||
layer = qgiscore.QgsVectorLayer(uri, layer_name, "ogr")
|
|
||||||
layer_exists = bool(layer and getattr(layer, "isValid", lambda: False)())
|
|
||||||
except Exception:
|
|
||||||
layer_exists = False
|
|
||||||
|
|
||||||
operation = "created"
|
|
||||||
|
|
||||||
if layer_exists:
|
|
||||||
# Zentrale Nutzerabfrage über Pruefmanager
|
|
||||||
# Erwartet Rückgabe: "overwrite" | "append" | "cancel"
|
|
||||||
try:
|
|
||||||
user_choice = self.pruefmanager.ask_overwrite_append_cancel(layer_name)
|
|
||||||
except Exception:
|
|
||||||
# Fallback: overwrite, falls Pruefmanager nicht verfügbar
|
|
||||||
user_choice = "overwrite"
|
|
||||||
|
|
||||||
if user_choice == "cancel":
|
|
||||||
operation = "skipped"
|
|
||||||
results.append({
|
|
||||||
"ident": ident,
|
|
||||||
"thema": thema,
|
|
||||||
"operation": operation,
|
|
||||||
"layer_path": f"{self.gpkg_path}|layername={layer_name}",
|
|
||||||
"feature_count": 0,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
|
|
||||||
if user_choice == "overwrite":
|
|
||||||
write_err = self._write_layer_to_gpkg(layer_name, features, mode="overwrite")
|
|
||||||
if write_err:
|
|
||||||
pe_err = pruef_ergebnis(
|
pe_err = pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung=f"Fehler beim Überschreiben von {layer_name}: {write_err}",
|
meldung=f"Fehler beim Schreiben des Layers {layer_name}: {err_msg}",
|
||||||
aktion="save_exception",
|
aktion="save_exception",
|
||||||
kontext={"ident": ident, "thema": thema, "error": write_err},
|
kontext={"ident": ident, "layer_name": layer_name},
|
||||||
)
|
)
|
||||||
self.pruefmanager.verarbeite(pe_err)
|
self.pruefmanager.verarbeite(pe_err)
|
||||||
operation = "skipped"
|
|
||||||
results.append({
|
|
||||||
"ident": ident,
|
|
||||||
"thema": thema,
|
|
||||||
"operation": operation,
|
|
||||||
"layer_path": f"{self.gpkg_path}|layername={layer_name}",
|
|
||||||
"feature_count": 0,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
operation = "overwritten"
|
|
||||||
|
|
||||||
elif user_choice == "append":
|
|
||||||
write_err = self._write_layer_to_gpkg(layer_name, features, mode="append")
|
|
||||||
if write_err:
|
|
||||||
pe_err = pruef_ergebnis(
|
|
||||||
ok=False,
|
|
||||||
meldung=f"Fehler beim Anhängen an {layer_name}: {write_err}",
|
|
||||||
aktion="save_exception",
|
|
||||||
kontext={"ident": ident, "thema": thema, "error": write_err},
|
|
||||||
)
|
|
||||||
self.pruefmanager.verarbeite(pe_err)
|
|
||||||
operation = "skipped"
|
|
||||||
results.append({
|
|
||||||
"ident": ident,
|
|
||||||
"thema": thema,
|
|
||||||
"operation": operation,
|
|
||||||
"layer_path": f"{self.gpkg_path}|layername={layer_name}",
|
|
||||||
"feature_count": 0,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
operation = "appended"
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Layer existiert nicht -> neu anlegen
|
|
||||||
write_err = self._write_layer_to_gpkg(layer_name, features, mode="create")
|
|
||||||
if write_err:
|
|
||||||
pe_err = pruef_ergebnis(
|
|
||||||
ok=False,
|
|
||||||
meldung=f"Fehler beim Erstellen von {layer_name}: {write_err}",
|
|
||||||
aktion="save_exception",
|
|
||||||
kontext={"ident": ident, "thema": thema, "error": write_err},
|
|
||||||
)
|
|
||||||
self.pruefmanager.verarbeite(pe_err)
|
|
||||||
operation = "skipped"
|
|
||||||
results.append({
|
|
||||||
"ident": ident,
|
|
||||||
"thema": thema,
|
|
||||||
"operation": operation,
|
|
||||||
"layer_path": f"{self.gpkg_path}|layername={layer_name}",
|
|
||||||
"feature_count": 0,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
operation = "created"
|
|
||||||
|
|
||||||
# Stilbehandlung (falls in processed_results referenziert)
|
|
||||||
style_written = False
|
|
||||||
style_path = None
|
|
||||||
for pe in processed_results:
|
|
||||||
try:
|
|
||||||
kontext = getattr(pe, "kontext", None) or {}
|
|
||||||
if kontext and kontext.get("ident") == ident:
|
|
||||||
style_path = kontext.get("stildatei") or kontext.get("Stildatei")
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Wenn der Stil vorhanden und valide ist, als Default in GPKG-Style-Tabelle ablegen
|
||||||
if style_path:
|
if style_path:
|
||||||
if not os.path.isabs(style_path):
|
self._store_style_in_gpkg(layer_name, style_path, layer)
|
||||||
base_dir = os.path.dirname(__file__)
|
|
||||||
style_path = os.path.join(base_dir, style_path)
|
|
||||||
write_style_fn = getattr(qgiscore, "write_style_to_gpkg", None)
|
|
||||||
if callable(write_style_fn):
|
|
||||||
try:
|
|
||||||
write_style_fn(self.gpkg_path, style_path, layer_name)
|
|
||||||
style_written = True
|
|
||||||
except Exception:
|
|
||||||
style_written = False
|
|
||||||
|
|
||||||
feature_count = len(features) if isinstance(features, list) else 0
|
|
||||||
|
|
||||||
|
# Erfolgsfall: Info für lade_Layer sammeln
|
||||||
|
layer_path = f"{self.gpkg_path}|layername={layer_name}"
|
||||||
results.append({
|
results.append({
|
||||||
|
"layer_path": layer_path,
|
||||||
|
"thema": layer_name,
|
||||||
"ident": ident,
|
"ident": ident,
|
||||||
"thema": thema,
|
"style_path": style_path,
|
||||||
"operation": operation,
|
|
||||||
"layer_path": f"{self.gpkg_path}|layername={layer_name}",
|
|
||||||
"feature_count": feature_count,
|
|
||||||
"style_written": style_written,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Lade Layer ins Projekt
|
# Lade Layer ins Projekt
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
@@ -288,6 +280,21 @@ class Datenschreiber:
|
|||||||
self.pruefmanager.verarbeite(pe_err)
|
self.pruefmanager.verarbeite(pe_err)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
style_path = info.get("style_path")
|
||||||
|
resolved_style_path = self._resolve_style_path(style_path)
|
||||||
|
if resolved_style_path:
|
||||||
|
try:
|
||||||
|
layer.loadNamedStyle(resolved_style_path)
|
||||||
|
layer.triggerRepaint()
|
||||||
|
except Exception as exc:
|
||||||
|
pe_warn = pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
meldung=f"Style konnte für {thema} nicht geladen werden: {exc}",
|
||||||
|
aktion="stil_laden_fehlgeschlagen",
|
||||||
|
kontext={"thema": thema, "style_path": resolved_style_path},
|
||||||
|
)
|
||||||
|
self.pruefmanager.verarbeite(pe_warn)
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
apply_style_fn = getattr(qgiscore, "apply_default_style_from_gpkg", None)
|
apply_style_fn = getattr(qgiscore, "apply_default_style_from_gpkg", None)
|
||||||
if callable(apply_style_fn):
|
if callable(apply_style_fn):
|
||||||
@@ -374,62 +381,67 @@ class Datenschreiber:
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# Hilfsfunktionen intern
|
# Hilfsfunktionen intern
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
def _write_layer_to_gpkg(self, layer_name: str, features: List[Any], mode: str = "create") -> Optional[str]:
|
def _write_layer_to_gpkg(
|
||||||
|
self,
|
||||||
|
layer_name: str,
|
||||||
|
layer: Any,
|
||||||
|
) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Interne Hilfsfunktion zum Schreiben eines Layers in das GPKG.
|
Schreibt einen QgsVectorLayer in die Ziel-GPKG.
|
||||||
|
|
||||||
Erwartete qgiscore-Funktion:
|
Voraussetzungen:
|
||||||
qgiscore.write_features_to_gpkg(gpkg_path, layer_name, features, mode)
|
- self.gpkg_path ist ein str
|
||||||
|
- layer ist ein gültiger QgsVectorLayer
|
||||||
"""
|
"""
|
||||||
write_fn = getattr(qgiscore, "write_features_to_gpkg", None)
|
|
||||||
if callable(write_fn):
|
if layer is None or not hasattr(layer, "isValid") or not layer.isValid():
|
||||||
|
return "Ungültiger Layer zum Schreiben übergeben"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
write_fn(self.gpkg_path, layer_name, features, mode)
|
|
||||||
return None
|
|
||||||
except Exception as exc:
|
|
||||||
return str(exc)
|
|
||||||
|
|
||||||
# Fallback: Verwende QgsVectorFileWriter, falls QGIS verfügbar
|
|
||||||
if getattr(qgiscore, "QGIS_AVAILABLE", False) and getattr(qgiscore, "QgsVectorFileWriter", None) is not None:
|
|
||||||
try:
|
|
||||||
# Minimaler Fallback: erwarte, dass 'features' eine Liste von QgsFeature ist
|
|
||||||
if not features:
|
|
||||||
# Erstelle leeren Layer-Eintrag (GPKG erlaubt leere Layer)
|
|
||||||
# Hier vereinfachen wir: writeAsVectorFormatV3 benötigt ein Layer-Objekt.
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Versuche, ein Memory-Layer aus dem ersten Feature zu ermitteln
|
|
||||||
first = features[0]
|
|
||||||
mem_layer = None
|
|
||||||
if hasattr(first, "fields") and hasattr(first, "geometry"):
|
|
||||||
# Wenn Features QgsFeature sind, versuchen wir, das zugehörige Layer zu nutzen
|
|
||||||
try:
|
|
||||||
mem_layer = first.layer() if hasattr(first, "layer") else None
|
|
||||||
except Exception:
|
|
||||||
mem_layer = None
|
|
||||||
|
|
||||||
if mem_layer is None:
|
|
||||||
return "Keine Feld-/Geometrie-Informationen zum Schreiben vorhanden"
|
|
||||||
|
|
||||||
opts = qgiscore.QgsVectorFileWriter.SaveVectorOptions()
|
opts = qgiscore.QgsVectorFileWriter.SaveVectorOptions()
|
||||||
opts.driverName = "GPKG"
|
opts.driverName = "GPKG"
|
||||||
opts.layerName = layer_name
|
opts.layerName = layer_name
|
||||||
opts.fileEncoding = "UTF-8"
|
opts.fileEncoding = "UTF-8"
|
||||||
if mode == "overwrite":
|
|
||||||
|
# Style in der GPKG speichern, wenn möglich
|
||||||
|
if hasattr(opts, "symbologyExport"):
|
||||||
|
try:
|
||||||
|
# QGIS: SymbologyExport-Wert z.B. QgsVectorFileWriter.SaveVectorOptions.Symbology
|
||||||
|
saveOpts = qgiscore.QgsVectorFileWriter.SaveVectorOptions
|
||||||
|
sym_val = getattr(saveOpts, "Symbology", None)
|
||||||
|
if sym_val is None:
|
||||||
|
sym_val = getattr(saveOpts, "SymbologyExport", None)
|
||||||
|
if sym_val is not None:
|
||||||
|
opts.symbologyExport = sym_val
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Datei existiert → Layer überschreiben
|
||||||
|
# Datei existiert nicht → neue GPKG anlegen
|
||||||
|
if not os.path.exists(self.gpkg_path):
|
||||||
opts.actionOnExistingFile = qgiscore.QgsVectorFileWriter.CreateOrOverwriteFile
|
opts.actionOnExistingFile = qgiscore.QgsVectorFileWriter.CreateOrOverwriteFile
|
||||||
else:
|
else:
|
||||||
opts.actionOnExistingFile = qgiscore.QgsVectorFileWriter.CreateOrOverwriteLayer
|
opts.actionOnExistingFile = qgiscore.QgsVectorFileWriter.CreateOrOverwriteLayer
|
||||||
|
|
||||||
err = qgiscore.QgsVectorFileWriter.writeAsVectorFormatV3(
|
err = qgiscore.QgsVectorFileWriter.writeAsVectorFormatV3(
|
||||||
mem_layer,
|
layer,
|
||||||
self.gpkg_path,
|
self.gpkg_path,
|
||||||
qgiscore.QgsProject.instance().transformContext(),
|
qgiscore.QgsProject.instance().transformContext(),
|
||||||
opts
|
opts,
|
||||||
)
|
)
|
||||||
if err != qgiscore.QgsVectorFileWriter.NoError:
|
|
||||||
return f"Fehler beim Schreiben (Code {err})"
|
# QGIS ≥3 liefert ein Tupel: (error_code, error_message, new_filename, new_layer_name)
|
||||||
|
if isinstance(err, tuple):
|
||||||
|
error_code = err[0]
|
||||||
|
error_msg = err[1] if len(err) > 1 else ""
|
||||||
|
else:
|
||||||
|
error_code = err
|
||||||
|
error_msg = ""
|
||||||
|
|
||||||
|
if error_code != qgiscore.QgsVectorFileWriter.NoError:
|
||||||
|
return f"Fehler beim Schreiben (Code {error_code}, msg='{error_msg}')"
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return str(exc)
|
return str(exc)
|
||||||
|
|
||||||
return "Keine Schreib-Funktion verfügbar (Wrapper nicht implementiert)"
|
|
||||||
|
|||||||
395
modules/LayerLoader.py
Normal file
395
modules/LayerLoader.py
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
"""sn_basis/modules/LayerLoader.py
|
||||||
|
|
||||||
|
Kapselt Layer-Erstellung, Raumfilter und Stil-Logik.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
import time
|
||||||
|
|
||||||
|
from sn_basis.functions.os_wrapper import normalize_path, is_absolute_path
|
||||||
|
from sn_basis.functions.qgiscore_wrapper import (
|
||||||
|
QgsVectorLayer,
|
||||||
|
QgsRasterLayer,
|
||||||
|
QgsFeatureRequest,
|
||||||
|
QgsProject,
|
||||||
|
QgsNetworkAccessManager,
|
||||||
|
QgsCoordinateTransform,
|
||||||
|
)
|
||||||
|
from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists
|
||||||
|
from sn_basis.modules.stilpruefer import Stilpruefer
|
||||||
|
from sn_basis.modules.layerpruefer import Layerpruefer
|
||||||
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
from sn_basis.functions import qt_wrapper as qt
|
||||||
|
|
||||||
|
|
||||||
|
class LayerLoader:
|
||||||
|
"""Lädt und filtert Layer aus Dienst-/Datenquellen."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
pruefmanager: Any,
|
||||||
|
stil_pruefer: Optional[Stilpruefer] = None,
|
||||||
|
layer_pruefer: Optional[Layerpruefer] = None,
|
||||||
|
) -> None:
|
||||||
|
self.pruefmanager = pruefmanager
|
||||||
|
self.stil_pruefer = stil_pruefer or Stilpruefer()
|
||||||
|
self.layer_pruefer = layer_pruefer or Layerpruefer()
|
||||||
|
|
||||||
|
_LAYER_TIMEOUT_MS = 30_000 # 30 Sekunden
|
||||||
|
|
||||||
|
def _was_canceled(self, cancel_callback: Optional[Any]) -> bool:
|
||||||
|
if not callable(cancel_callback):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return bool(cancel_callback())
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _process_events(self) -> None:
|
||||||
|
try:
|
||||||
|
if hasattr(qt, "QCoreApplication") and hasattr(qt.QCoreApplication, "processEvents"):
|
||||||
|
qt.QCoreApplication.processEvents()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _transform_geometry_to_layer_crs(self, geometry: Any, source_layer: Any, target_layer: Any) -> Any:
|
||||||
|
if geometry is None or source_layer is None or target_layer is None:
|
||||||
|
return geometry
|
||||||
|
|
||||||
|
if QgsCoordinateTransform is None or QgsProject is None:
|
||||||
|
return geometry
|
||||||
|
|
||||||
|
try:
|
||||||
|
source_crs = source_layer.crs() if hasattr(source_layer, "crs") else None
|
||||||
|
target_crs = target_layer.crs() if hasattr(target_layer, "crs") else None
|
||||||
|
if source_crs is None or target_crs is None:
|
||||||
|
return geometry
|
||||||
|
|
||||||
|
source_authid = source_crs.authid() if hasattr(source_crs, "authid") else None
|
||||||
|
target_authid = target_crs.authid() if hasattr(target_crs, "authid") else None
|
||||||
|
if source_authid and target_authid and source_authid == target_authid:
|
||||||
|
return geometry
|
||||||
|
|
||||||
|
ct = QgsCoordinateTransform(source_crs, target_crs, QgsProject.instance())
|
||||||
|
if hasattr(geometry, "clone") and callable(getattr(geometry, "clone")):
|
||||||
|
geom_copy = geometry.clone()
|
||||||
|
else:
|
||||||
|
geom_copy = geometry
|
||||||
|
geom_copy.transform(ct)
|
||||||
|
return geom_copy
|
||||||
|
except Exception:
|
||||||
|
return geometry
|
||||||
|
|
||||||
|
def _transform_extent_to_layer_crs(self, extent: Any, source_layer: Any, target_layer: Any) -> Any:
|
||||||
|
if extent is None or source_layer is None or target_layer is None:
|
||||||
|
return extent
|
||||||
|
|
||||||
|
if QgsCoordinateTransform is None or QgsProject is None:
|
||||||
|
return extent
|
||||||
|
|
||||||
|
try:
|
||||||
|
source_crs = source_layer.crs() if hasattr(source_layer, "crs") else None
|
||||||
|
target_crs = target_layer.crs() if hasattr(target_layer, "crs") else None
|
||||||
|
if source_crs is None or target_crs is None:
|
||||||
|
return extent
|
||||||
|
|
||||||
|
source_authid = source_crs.authid() if hasattr(source_crs, "authid") else None
|
||||||
|
target_authid = target_crs.authid() if hasattr(target_crs, "authid") else None
|
||||||
|
if source_authid and target_authid and source_authid == target_authid:
|
||||||
|
return extent
|
||||||
|
|
||||||
|
ct = QgsCoordinateTransform(source_crs, target_crs, QgsProject.instance())
|
||||||
|
if hasattr(ct, "transformBoundingBox"):
|
||||||
|
return ct.transformBoundingBox(extent)
|
||||||
|
return extent
|
||||||
|
except Exception:
|
||||||
|
return extent
|
||||||
|
|
||||||
|
def create_layer(self, provider: str, link: str, thema: str) -> Optional[QgsVectorLayer]:
|
||||||
|
provider_lower = provider.lower() if provider else ""
|
||||||
|
layer = None
|
||||||
|
|
||||||
|
# Netzwerk-Timeout für alle netzwerkbasierten Provider setzen
|
||||||
|
if provider_lower in ("wfs", "wms", "rest"):
|
||||||
|
try:
|
||||||
|
nam = QgsNetworkAccessManager.instance()
|
||||||
|
if hasattr(nam, "setTimeout"):
|
||||||
|
nam.setTimeout(self._LAYER_TIMEOUT_MS)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if provider_lower == "wfs":
|
||||||
|
uri = link if link.strip().lower().startswith("url=") else f"url={link}"
|
||||||
|
layer = QgsVectorLayer(uri, thema, "WFS")
|
||||||
|
elif provider_lower == "wms":
|
||||||
|
uri = link if link.strip().lower().startswith("url=") else f"url={link}"
|
||||||
|
layer = QgsRasterLayer(uri, thema, "wms")
|
||||||
|
elif provider_lower in ("ogr", "gpkg", "shp", "geojson"):
|
||||||
|
layer = QgsVectorLayer(link, thema, "ogr")
|
||||||
|
elif provider_lower == "rest":
|
||||||
|
rest_link = link.strip()
|
||||||
|
if rest_link.lower().endswith("/featureserver"):
|
||||||
|
rest_link = rest_link.rstrip("/") + "/0"
|
||||||
|
uri = rest_link if rest_link.lower().startswith("url=") else f"url={rest_link}"
|
||||||
|
layer = QgsVectorLayer(uri, thema, "arcgisfeatureserver")
|
||||||
|
else:
|
||||||
|
layer = QgsVectorLayer(link, thema, "ogr")
|
||||||
|
except Exception as exc:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Fehler beim Erstellen des Layers {thema}: {exc}",
|
||||||
|
aktion="layer_nicht_verfuegbar",
|
||||||
|
kontext={"provider": provider, "link": link},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not layer or not layer.isValid():
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Layer {thema} (Provider={provider}) konnte nicht geladen werden."
|
||||||
|
,aktion="layer_nicht_verfuegbar",
|
||||||
|
kontext={"provider": provider, "link": link},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return layer
|
||||||
|
|
||||||
|
def apply_style(self, layer: QgsVectorLayer, style_path: Optional[str]) -> None:
|
||||||
|
if not style_path or layer is None or not layer.isValid():
|
||||||
|
return
|
||||||
|
|
||||||
|
if not style_path.strip():
|
||||||
|
return
|
||||||
|
|
||||||
|
if not is_absolute_path(style_path):
|
||||||
|
plugin_root = get_plugin_root()
|
||||||
|
style_path = str(join_path(plugin_root, "sn_plan41", "assets", style_path))
|
||||||
|
|
||||||
|
# normalize path for consistency
|
||||||
|
style_path = str(normalize_path(style_path))
|
||||||
|
|
||||||
|
# Debug: welche Stil-Datei wird geprüft?
|
||||||
|
print(f"[LayerLoader] Überprüfe Stildatei: '{style_path}'")
|
||||||
|
|
||||||
|
if file_exists(style_path):
|
||||||
|
try:
|
||||||
|
layer.loadNamedStyle(style_path)
|
||||||
|
layer.triggerRepaint()
|
||||||
|
except Exception as exc:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Fehler beim Stil-Laden für {layer.name()}: {exc}",
|
||||||
|
aktion="stil_laden_fehlgeschlagen",
|
||||||
|
kontext={"thema": layer.name(), "style_path": style_path},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
meldung=f"Stildatei nicht gefunden (optional): {style_path}",
|
||||||
|
aktion="stil_nicht_gefunden",
|
||||||
|
kontext={"thema": layer.name(), "style_path": style_path},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_by_extent(self, layer: QgsVectorLayer, extent, cancel_callback: Optional[Any] = None, source_layer: Optional[Any] = None) -> Optional[QgsVectorLayer]:
|
||||||
|
"""Beschneidet <layer> auf die rechteckige Ausdehnung <extent>.
|
||||||
|
|
||||||
|
Diese Methode verwendet einen einfachen BBOX-Filter. Für komplexere
|
||||||
|
Raumeinschränkungen (z.B. Verfahrensgebiet) sollte stattdessen
|
||||||
|
:meth:`filter_by_layer` verwendet werden, da dort echte Geometrie-Tests
|
||||||
|
stattfinden.
|
||||||
|
"""
|
||||||
|
if not layer or not layer.isValid() or extent is None:
|
||||||
|
return layer
|
||||||
|
|
||||||
|
if layer.type() != QgsVectorLayer.VectorLayer:
|
||||||
|
return layer
|
||||||
|
|
||||||
|
extent_for_layer = self._transform_extent_to_layer_crs(extent, source_layer, layer)
|
||||||
|
request = QgsFeatureRequest().setFilterRect(extent_for_layer)
|
||||||
|
if hasattr(request, "setTimeout"):
|
||||||
|
try:
|
||||||
|
request.setTimeout(self._LAYER_TIMEOUT_MS)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
start = time.monotonic()
|
||||||
|
features: List[Any] = []
|
||||||
|
try:
|
||||||
|
for feat in layer.getFeatures(request):
|
||||||
|
if self._was_canceled(cancel_callback):
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Abbruch beim Raumfilter (BBOX) für {layer.name()}",
|
||||||
|
aktion="needs_user_action",
|
||||||
|
kontext={"thema": layer.name()},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
elapsed_ms = int((time.monotonic() - start) * 1000)
|
||||||
|
if elapsed_ms >= self._LAYER_TIMEOUT_MS:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Timeout beim Raumfilter (BBOX) für {layer.name()} nach {self._LAYER_TIMEOUT_MS // 1000}s",
|
||||||
|
aktion="url_nicht_erreichbar",
|
||||||
|
kontext={"thema": layer.name(), "timeout_s": self._LAYER_TIMEOUT_MS // 1000},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
features.append(feat)
|
||||||
|
if len(features) % 100 == 0:
|
||||||
|
self._process_events()
|
||||||
|
except Exception as exc:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Fehler beim Lesen der Features für {layer.name()}: {exc}",
|
||||||
|
aktion="layer_nicht_verfuegbar",
|
||||||
|
kontext={"thema": layer.name()},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not features:
|
||||||
|
return None
|
||||||
|
|
||||||
|
geom_type_map = {0: "Point", 1: "LineString", 2: "Polygon"}
|
||||||
|
geom_type = geom_type_map.get(layer.geometryType(), "Polygon")
|
||||||
|
uri = f"{geom_type}?crs={layer.crs().authid()}"
|
||||||
|
filtered_layer = QgsVectorLayer(uri, f"{layer.name()}_bbox", "memory")
|
||||||
|
if not filtered_layer or not filtered_layer.isValid():
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Fehler beim Erzeugen des Filter-Layers für {layer.name()}",
|
||||||
|
aktion="filterlayer_nicht_erzeugt",
|
||||||
|
kontext={"thema": layer.name()},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
provider = filtered_layer.dataProvider()
|
||||||
|
provider.addAttributes(layer.fields())
|
||||||
|
filtered_layer.updateFields()
|
||||||
|
provider.addFeatures(features)
|
||||||
|
filtered_layer.updateExtents()
|
||||||
|
|
||||||
|
return filtered_layer
|
||||||
|
|
||||||
|
def filter_by_layer(self, layer: QgsVectorLayer, filter_layer: QgsVectorLayer, cancel_callback: Optional[Any] = None) -> Optional[QgsVectorLayer]:
|
||||||
|
"""Beschneidet <layer> auf die tatsächliche Geometrie des
|
||||||
|
<filter_layer>.
|
||||||
|
|
||||||
|
Diese Methode wird z.B. für das Verfahrensgebiet verwendet, damit nicht
|
||||||
|
die gesamte Bounding-Box, sondern nur die echten Flächen als Raumfilter
|
||||||
|
gelten. Wenn der Filter-Layer mehrere Features enthält, werden deren
|
||||||
|
Geometrien zu einem Multi-Geom vereinigt.
|
||||||
|
"""
|
||||||
|
if not layer or not layer.isValid() or not filter_layer or not filter_layer.isValid():
|
||||||
|
return layer
|
||||||
|
|
||||||
|
if layer.type() != QgsVectorLayer.VectorLayer:
|
||||||
|
return layer
|
||||||
|
|
||||||
|
# vereinigte Geometrie aller Features im Filter-Layer
|
||||||
|
union_geom = None
|
||||||
|
for f in filter_layer.getFeatures():
|
||||||
|
try:
|
||||||
|
geom = self._transform_geometry_to_layer_crs(f.geometry(), filter_layer, layer)
|
||||||
|
if union_geom is None:
|
||||||
|
union_geom = geom
|
||||||
|
else:
|
||||||
|
union_geom = union_geom.combine(geom)
|
||||||
|
except Exception:
|
||||||
|
# bei einem Fehler einfach weiterfahren
|
||||||
|
continue
|
||||||
|
|
||||||
|
if union_geom is None or union_geom.isEmpty():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# nun alle Features aus <layer> nehmen, deren Geometrie sich schneidet
|
||||||
|
filtered = []
|
||||||
|
request = QgsFeatureRequest().setFilterRect(union_geom.boundingBox())
|
||||||
|
if hasattr(request, "setTimeout"):
|
||||||
|
try:
|
||||||
|
request.setTimeout(self._LAYER_TIMEOUT_MS)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
start = time.monotonic()
|
||||||
|
for f in layer.getFeatures(request):
|
||||||
|
if self._was_canceled(cancel_callback):
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Abbruch beim Raumfilter (Geometrie) für {layer.name()}",
|
||||||
|
aktion="needs_user_action",
|
||||||
|
kontext={"thema": layer.name()},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
elapsed_ms = int((time.monotonic() - start) * 1000)
|
||||||
|
if elapsed_ms >= self._LAYER_TIMEOUT_MS:
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Timeout beim Raumfilter (Geometrie) für {layer.name()} nach {self._LAYER_TIMEOUT_MS // 1000}s",
|
||||||
|
aktion="url_nicht_erreichbar",
|
||||||
|
kontext={"thema": layer.name(), "timeout_s": self._LAYER_TIMEOUT_MS // 1000},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if f.geometry() and f.geometry().intersects(union_geom):
|
||||||
|
filtered.append(f)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(filtered) % 100 == 0:
|
||||||
|
self._process_events()
|
||||||
|
|
||||||
|
if not filtered:
|
||||||
|
return None
|
||||||
|
|
||||||
|
geom_type_map = {0: "Point", 1: "LineString", 2: "Polygon"}
|
||||||
|
geom_type = geom_type_map.get(layer.geometryType(), "Polygon")
|
||||||
|
uri = f"{geom_type}?crs={layer.crs().authid()}"
|
||||||
|
filtered_layer = QgsVectorLayer(uri, f"{layer.name()}_filtered", "memory")
|
||||||
|
if not filtered_layer or not filtered_layer.isValid():
|
||||||
|
self.pruefmanager.verarbeite(
|
||||||
|
pruef_ergebnis(
|
||||||
|
ok=False,
|
||||||
|
meldung=f"Fehler beim Erzeugen des Filter-Layers für {layer.name()}",
|
||||||
|
aktion="filterlayer_nicht_erzeugt",
|
||||||
|
kontext={"thema": layer.name()},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
provider = filtered_layer.dataProvider()
|
||||||
|
provider.addAttributes(layer.fields())
|
||||||
|
filtered_layer.updateFields()
|
||||||
|
provider.addFeatures(filtered)
|
||||||
|
filtered_layer.updateExtents()
|
||||||
|
|
||||||
|
return filtered_layer
|
||||||
|
|
||||||
|
def add_to_project(self, layer: QgsVectorLayer) -> None:
|
||||||
|
if layer and layer.isValid():
|
||||||
|
QgsProject.instance().addMapLayer(layer)
|
||||||
@@ -1,66 +1,45 @@
|
|||||||
|
"""
|
||||||
|
sn_basis/modules/Pruefmanager.py
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Optional, Any
|
from typing import Optional, Any
|
||||||
|
|
||||||
from sn_basis.functions import (
|
from sn_basis.functions import ask_yes_no, info, warning, error, ask_overwrite_append_cancel_custom
|
||||||
ask_yes_no,
|
|
||||||
info,
|
|
||||||
warning,
|
|
||||||
error,
|
|
||||||
set_layer_visible,
|
|
||||||
)
|
|
||||||
|
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
|
||||||
|
print("DEBUG: Pruefmanager DATEI GELADEN:", __file__)
|
||||||
|
|
||||||
class Pruefmanager:
|
class Pruefmanager:
|
||||||
"""
|
def __init__(self, ui_modus: str = "qgis", parent: Optional[Any] = None) -> None:
|
||||||
Zentrale Verarbeitung von pruef_ergebnis-Objekten.
|
|
||||||
|
|
||||||
Erwartete öffentliche API (verwendet von Core-Komponenten wie DataGrabber):
|
|
||||||
- report_error(thema, meldung, *, aktion: Optional[PruefAktion]=None, kontext=None) -> None
|
|
||||||
- request_decision(pruef_res) -> str
|
|
||||||
- report_summary(summary: dict) -> None
|
|
||||||
- verarbeite(ergebnis: pruef_ergebnis) -> pruef_ergebnis
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, ui_modus: str = "qgis", parent: Optional[Any] = None):
|
|
||||||
self.ui_modus = ui_modus
|
self.ui_modus = ui_modus
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Basis-API: Meldungen / Zusammenfassungen
|
# Meldungen / Zusammenfassungen
|
||||||
# ---------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def report_error(self, thema: str, meldung: str, *, aktion: Optional[PruefAktion] = None, kontext: Optional[Any] = None) -> None:
|
def report_error(
|
||||||
"""
|
self,
|
||||||
Einheitliche Meldung für Fehler/Warnungen aus dem Core.
|
thema: str,
|
||||||
Keine Rückgabe; dient als zentraler Hook für Logging/UI.
|
meldung: str,
|
||||||
"""
|
*,
|
||||||
|
aktion: Optional[PruefAktion] = None,
|
||||||
|
kontext: Optional[Any] = None,
|
||||||
|
) -> None:
|
||||||
critical_actions = {
|
critical_actions = {
|
||||||
"netzwerkfehler",
|
"netzwerkfehler", "pruefe_exception", "save_exception",
|
||||||
"pruefe_exception",
|
"layer_create_failed", "read_error", "open_error",
|
||||||
"save_exception",
|
|
||||||
"layer_create_failed",
|
|
||||||
"read_error",
|
|
||||||
"open_error",
|
|
||||||
}
|
}
|
||||||
warn_actions = {
|
warn_actions = {
|
||||||
"datei_nicht_gefunden",
|
"datei_nicht_gefunden", "pfad_nicht_gefunden", "url_nicht_erreichbar",
|
||||||
"pfad_nicht_gefunden",
|
"falsche_endung", "kein_header", "kein_arbeitsblatt",
|
||||||
"url_nicht_erreichbar",
|
|
||||||
"falsche_endung",
|
|
||||||
"kein_header",
|
|
||||||
"kein_arbeitsblatt",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if aktion in critical_actions:
|
if aktion in critical_actions:
|
||||||
error(thema, meldung)
|
error(thema, meldung)
|
||||||
return
|
return
|
||||||
|
|
||||||
if aktion in warn_actions:
|
if aktion in warn_actions:
|
||||||
warning(thema, meldung)
|
warning(thema, meldung)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Default: informative Warnung
|
|
||||||
warning(thema, meldung)
|
warning(thema, meldung)
|
||||||
|
|
||||||
def report_summary(self, summary: dict) -> None:
|
def report_summary(self, summary: dict) -> None:
|
||||||
@@ -75,202 +54,209 @@ class Pruefmanager:
|
|||||||
f"Dienste ausserhalb: {len(ausserhalb)}\n"
|
f"Dienste ausserhalb: {len(ausserhalb)}\n"
|
||||||
f"Fehler: {len(fehler)}"
|
f"Fehler: {len(fehler)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
info("DataGrabber Zusammenfassung", message)
|
info("DataGrabber Zusammenfassung", message)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Entscheidungs-API
|
# Allgemeine Nutzerinteraktionen
|
||||||
# ---------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def request_decision(self, pruef_res: Any) -> str:
|
def zeige_hinweis(self, titel: str, meldung: str) -> None:
|
||||||
"""
|
"""Zeigt eine modale Hinweismeldung mit OK-Button."""
|
||||||
Synchronously request a decision from the user (or return a default in headless mode).
|
from sn_basis.functions.dialog_wrapper import show_info_dialog
|
||||||
|
show_info_dialog(titel, meldung, parent=self.parent)
|
||||||
|
|
||||||
Returns one of:
|
def frage_ja_nein(self, titel: str, meldung: str, default: bool = True) -> bool:
|
||||||
- "abort"
|
"""Stellt eine Ja/Nein-Frage. Gibt True zurück, wenn der Nutzer Ja wählt."""
|
||||||
- "continue"
|
if self.ui_modus != "qgis":
|
||||||
- "temporaer_erzeugen"
|
return default
|
||||||
- "ignore"
|
return ask_yes_no(titel, meldung, default=default, parent=self.parent)
|
||||||
"""
|
|
||||||
aktion = getattr(pruef_res, "aktion", None)
|
|
||||||
meldung = getattr(pruef_res, "meldung", str(pruef_res))
|
|
||||||
|
|
||||||
|
def frage_text(self, titel: str, meldung: str, default_text: str = "") -> tuple[str, bool]:
|
||||||
|
"""Fragt einen Textwert ab und gibt Text + OK-Status zurück."""
|
||||||
|
from sn_basis.functions.dialog_wrapper import ask_text
|
||||||
|
if self.ui_modus != "qgis":
|
||||||
|
return default_text, True
|
||||||
|
return ask_text(titel, meldung, default_text=default_text, parent=self.parent)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# VERFAHRENS-DB-spezifische Entscheidungen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _handle_datei_existiert(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
|
||||||
|
"""Handhabt das Szenario, dass die Ziel-Verfahrens-DB bereits existiert.
|
||||||
|
|
||||||
|
Zeigt einen einzigen Dialog mit drei Optionen an:
|
||||||
|
- **Überschreiben**: Bestehende Layer ersetzen (entspricht YES)
|
||||||
|
- **Anhängen**: Neue Layer zur Datei hinzufügen (entspricht NO)
|
||||||
|
- **Abbrechen**: Vorgang beenden (entspricht CANCEL)
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ergebnis : pruef_ergebnis
|
||||||
|
Eingabe-Ergebnis mit Dateipfad im ``kontext``-Attribut.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
pruef_ergebnis
|
||||||
|
Ergebnis mit Aktion:
|
||||||
|
- ``datei_existiert_ueberschreiben``
|
||||||
|
- ``datei_existiert_anhaengen``
|
||||||
|
- ``datei_existiert_ueberspringen`` (für Cancel-Fall)
|
||||||
|
"""
|
||||||
|
if self.ui_modus != "qgis":
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
pfad = ergebnis.kontext
|
||||||
|
pfad_str = str(pfad) if pfad else "unbekannt"
|
||||||
|
|
||||||
|
titel = "Verfahrens-DB existiert bereits"
|
||||||
|
meldung = (
|
||||||
|
f"Die Datei '{pfad_str}' existiert bereits.\n\n"
|
||||||
|
"Was soll geschehen?\n\n"
|
||||||
|
"• **Überschreiben**: Bestehende Layer ersetzen\n"
|
||||||
|
"• **Anhängen**: Neue Layer hinzufügen\n"
|
||||||
|
"• **Abbrechen**: Vorgang beenden"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Einzelner Dialog mit drei Optionen
|
||||||
|
entscheidung = ask_overwrite_append_cancel_custom(
|
||||||
|
parent=self.parent,
|
||||||
|
title=titel,
|
||||||
|
message=meldung
|
||||||
|
)
|
||||||
|
|
||||||
|
if entscheidung == "overwrite":
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
aktion="datei_existiert_ueberschreiben",
|
||||||
|
kontext=ergebnis.kontext,
|
||||||
|
)
|
||||||
|
elif entscheidung == "append":
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
aktion="datei_existiert_anhaengen",
|
||||||
|
kontext=ergebnis.kontext,
|
||||||
|
)
|
||||||
|
else: # cancel
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
aktion="datei_existiert_ueberspringen",
|
||||||
|
kontext=ergebnis.kontext,
|
||||||
|
)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Basis-Entscheidungen (KORREKT: → pruef_ergebnis)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _handle_basic_decision(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
|
||||||
|
"""Basis-Entscheidung für einfache Ja/Nein-Fragen."""
|
||||||
|
print(f"DEBUG _handle_basic_decision: aktion='{ergebnis.aktion}', ui_modus='{self.ui_modus}'")
|
||||||
|
|
||||||
|
if self.ui_modus != "qgis":
|
||||||
|
print("DEBUG: Nicht QGIS → ergebnis unverändert")
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
title_map = {
|
||||||
|
"leereingabe_erlaubt": "Ohne Eingabe fortfahren",
|
||||||
|
"standarddatei_vorschlagen": "Standarddatei verwenden",
|
||||||
|
"temporaer_erlaubt": "Temporäre Layer erzeugen",
|
||||||
|
"layer_unsichtbar": "Layer einblenden",
|
||||||
|
}
|
||||||
|
|
||||||
|
title = title_map.get(ergebnis.aktion, "Entscheidung erforderlich")
|
||||||
|
meldung = ergebnis.meldung or ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"DEBUG ask_yes_no: title='{title}', meldung='{meldung[:50]}...'")
|
||||||
|
yes = ask_yes_no(title, meldung, default=False, parent=self.parent)
|
||||||
|
print(f"DEBUG ask_yes_no: yes={yes}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG ask_yes_no Exception: {e}")
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
if not yes:
|
||||||
|
print("DEBUG: Nutzer sagte Nein → ok=False")
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
# Nutzer sagte Ja
|
||||||
|
if ergebnis.aktion == "temporaer_erlaubt":
|
||||||
|
print("DEBUG: temporaer_erlaubt bestätigt → ok=True")
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
aktion="temporaer_erlaubt",
|
||||||
|
kontext=ergebnis.kontext
|
||||||
|
)
|
||||||
|
|
||||||
|
print("DEBUG: Andere Aktion bestätigt → ok=True, aktion='ok'")
|
||||||
|
return pruef_ergebnis(
|
||||||
|
ok=True,
|
||||||
|
aktion="ok",
|
||||||
|
kontext=ergebnis.kontext
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Hauptlogik: verarbeite() (KORRIGIERT!)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def verarbeite(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
|
||||||
|
print("🔥 verarbeite() START")
|
||||||
|
print("DEBUG Pruefmanager:", ergebnis.ok, ergebnis.aktion)
|
||||||
|
print("DEBUG ergebnis.aktion TYPE:", type(ergebnis.aktion), repr(ergebnis.aktion))
|
||||||
|
|
||||||
|
# 1. Erfolg → direkt weiter
|
||||||
|
print("🔍 Schritt 1: Prüfe ergebnis.ok =", ergebnis.ok)
|
||||||
|
if ergebnis.ok:
|
||||||
|
print("✅ Schritt 1: ok=True → return")
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
# 2. VERFAHRENS-DB: Bestehende Datei
|
||||||
|
print("🔍 Schritt 2: Prüfe datei_existiert =", ergebnis.aktion == "datei_existiert")
|
||||||
|
if ergebnis.aktion == "datei_existiert":
|
||||||
|
print("✅ Schritt 2: _handle_datei_existiert")
|
||||||
|
return self._handle_datei_existiert(ergebnis)
|
||||||
|
|
||||||
|
# 3. Basis interaktive Aktionen
|
||||||
|
print("🔍 Schritt 3: Definiere interactive_actions")
|
||||||
interactive_actions = {
|
interactive_actions = {
|
||||||
"leereingabe_erlaubt",
|
"leereingabe_erlaubt",
|
||||||
"standarddatei_vorschlagen",
|
"standarddatei_vorschlagen",
|
||||||
"temporaer_erlaubt",
|
"temporaer_erlaubt",
|
||||||
"layer_unsichtbar",
|
"layer_unsichtbar",
|
||||||
}
|
}
|
||||||
|
print("DEBUG interactive_actions:", repr(interactive_actions))
|
||||||
|
print("DEBUG ergebnis.aktion in interactive_actions?", ergebnis.aktion in interactive_actions)
|
||||||
|
|
||||||
if aktion in interactive_actions:
|
if ergebnis.aktion in interactive_actions:
|
||||||
if self.ui_modus == "qgis":
|
print("✅ Schritt 3: Interaktive Aktion → _handle_basic_decision")
|
||||||
title_map = {
|
decision = self._handle_basic_decision(ergebnis)
|
||||||
"leereingabe_erlaubt": "Ohne Eingabe fortfahren",
|
print(f"DEBUG: _handle_basic_decision Ergebnis: ok={decision.ok}, aktion='{decision.aktion}'")
|
||||||
"standarddatei_vorschlagen": "Standarddatei verwenden",
|
return decision
|
||||||
"temporaer_erlaubt": "Temporäre Datei erzeugen",
|
|
||||||
"layer_unsichtbar": "Layer einblenden",
|
|
||||||
}
|
|
||||||
title = title_map.get(aktion, "Entscheidung erforderlich")
|
|
||||||
try:
|
|
||||||
yes = ask_yes_no(title, meldung, default=False, parent=self.parent)
|
|
||||||
except Exception:
|
|
||||||
return "abort"
|
|
||||||
if yes:
|
|
||||||
if aktion == "temporaer_erlaubt":
|
|
||||||
return "temporaer_erzeugen"
|
|
||||||
return "continue"
|
|
||||||
return "abort"
|
|
||||||
|
|
||||||
if self.ui_modus == "headless":
|
# 4. Fehler behandeln
|
||||||
return "abort"
|
print("❌ Schritt 4: FEHLER BEHANDELN")
|
||||||
|
self.report_error(
|
||||||
|
thema=ergebnis.aktion or "pruefung",
|
||||||
|
meldung=ergebnis.meldung or "",
|
||||||
|
aktion=ergebnis.aktion,
|
||||||
|
kontext=ergebnis.kontext,
|
||||||
|
)
|
||||||
|
print("🔥 verarbeite() ENDE mit ok=False")
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
informational_actions = {
|
def _ask_use_or_replace_pufferlayer(self) -> str:
|
||||||
"leer",
|
|
||||||
"datei_nicht_gefunden",
|
|
||||||
"pfad_nicht_gefunden",
|
|
||||||
"url_nicht_erreichbar",
|
|
||||||
"netzwerkfehler",
|
|
||||||
"falscher_geotyp",
|
|
||||||
"layer_leer",
|
|
||||||
"falscher_layertyp",
|
|
||||||
"falsches_crs",
|
|
||||||
"felder_fehlen",
|
|
||||||
"datenquelle_unerwartet",
|
|
||||||
"layer_nicht_editierbar",
|
|
||||||
"kein_header",
|
|
||||||
"kein_arbeitsblatt",
|
|
||||||
"read_error",
|
|
||||||
"open_error",
|
|
||||||
"pflichtfelder_fehlen",
|
|
||||||
}
|
|
||||||
if aktion in informational_actions:
|
|
||||||
return "abort"
|
|
||||||
|
|
||||||
return "abort"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
# Höhere Abstraktion: verarbeite
|
|
||||||
# ---------------------------------------------------------------------
|
|
||||||
def verarbeite(self, ergebnis: pruef_ergebnis) -> pruef_ergebnis:
|
|
||||||
"""
|
"""
|
||||||
Verarbeitet ein pruef_ergebnis-Objekt und führt ggf. Nutzerinteraktion durch.
|
Fragt den Nutzer, ob ein vorhandener Pufferlayer verwendet
|
||||||
Liefert ein ggf. modifiziertes pruef_ergebnis zurück.
|
oder ersetzt werden soll.
|
||||||
"""
|
|
||||||
if ergebnis.ok:
|
|
||||||
return ergebnis
|
|
||||||
|
|
||||||
aktion = ergebnis.aktion
|
Returns
|
||||||
kontext = ergebnis.kontext
|
|
||||||
meldung = ergebnis.meldung
|
|
||||||
|
|
||||||
# Zentrale Meldung
|
|
||||||
self.report_error(aktion or "pruefung", meldung or "", aktion=aktion, kontext=kontext)
|
|
||||||
|
|
||||||
# Interaktive Entscheidungen
|
|
||||||
if aktion in ("leereingabe_erlaubt", "standarddatei_vorschlagen", "temporaer_erlaubt", "layer_unsichtbar"):
|
|
||||||
decision = self.request_decision(ergebnis)
|
|
||||||
if decision == "temporaer_erzeugen":
|
|
||||||
return pruef_ergebnis(ok=True, meldung="Temporäre Datei soll erzeugt werden.", aktion="temporaer_erzeugen", kontext=None)
|
|
||||||
if decision == "continue":
|
|
||||||
return pruef_ergebnis(ok=True, meldung="Fortgefahren.", aktion="ok", kontext=kontext)
|
|
||||||
return ergebnis # abort / unverändert
|
|
||||||
|
|
||||||
# Spezielle Excel/Importer-Fälle: klare Meldungen, keine interaktive Entscheidung
|
|
||||||
if aktion == "kein_header":
|
|
||||||
warning("Excel-Import", meldung or "")
|
|
||||||
return ergebnis
|
|
||||||
|
|
||||||
if aktion == "kein_arbeitsblatt":
|
|
||||||
warning("Excel-Import", meldung or "")
|
|
||||||
return ergebnis
|
|
||||||
|
|
||||||
if aktion in ("read_error", "open_error"):
|
|
||||||
error("Excel-Import", meldung or "")
|
|
||||||
return ergebnis
|
|
||||||
|
|
||||||
if aktion == "datei_nicht_gefunden":
|
|
||||||
warning("Datei nicht gefunden", meldung or "")
|
|
||||||
return ergebnis
|
|
||||||
|
|
||||||
# Spezieller Fall: layer_unsichtbar (falls nicht interaktiv behandelt)
|
|
||||||
if aktion == "layer_unsichtbar":
|
|
||||||
if kontext is not None:
|
|
||||||
try:
|
|
||||||
set_layer_visible(kontext, True)
|
|
||||||
return pruef_ergebnis(ok=True, meldung="Layer wurde eingeblendet.", aktion="ok", kontext=kontext)
|
|
||||||
except Exception:
|
|
||||||
return ergebnis
|
|
||||||
return ergebnis
|
|
||||||
|
|
||||||
# Standard: keine Änderung
|
|
||||||
return ergebnis
|
|
||||||
def ask_overwrite_append_cancel(self, layer_name: str, default: str = "overwrite") -> str:
|
|
||||||
"""
|
|
||||||
Zeigt dem Nutzer eine Auswahl für einen bereits existierenden Layer an.
|
|
||||||
|
|
||||||
Rückgabe
|
|
||||||
-------
|
-------
|
||||||
str
|
str
|
||||||
Einer der Werte: "overwrite", "append", "cancel".
|
"verwenden", "ersetzen" oder "abbrechen"
|
||||||
|
|
||||||
Verhalten
|
|
||||||
--------
|
|
||||||
- Verwendet bevorzugt die UI-Wrapper-Funktion `qt_wrapper` / `qgisui_wrapper`,
|
|
||||||
falls vorhanden (z. B. ein QMessageBox-Dialog mit drei Buttons).
|
|
||||||
- Im Mock- oder Headless-Modus (kein Qt/QGIS verfügbar) wird der übergebene
|
|
||||||
`default`-Wert zurückgegeben.
|
|
||||||
- Alle Nutzerinteraktionen laufen über diese zentrale Methode, damit das
|
|
||||||
Plugin an einer Stelle gesteuert und ggf. getested werden kann.
|
|
||||||
|
|
||||||
Parameter
|
|
||||||
---------
|
|
||||||
layer_name:
|
|
||||||
Anzeigename des Layers, der bereits existiert (wird im Dialog angezeigt).
|
|
||||||
default:
|
|
||||||
Rückgabewert im Headless/Mock-Modus oder wenn der Dialog nicht verfügbar ist.
|
|
||||||
Gültige Werte: "overwrite", "append", "cancel". Standard: "overwrite".
|
|
||||||
"""
|
"""
|
||||||
# Validierung des Defaults
|
ergebnis = pruef_ergebnis(
|
||||||
if default not in ("overwrite", "append", "cancel"):
|
ok=False,
|
||||||
default = "overwrite"
|
aktion="layer_existiert",
|
||||||
|
meldung="Ein Pufferlayer ist bereits vorhanden.",
|
||||||
|
)
|
||||||
|
|
||||||
# Versuche, eine UI-Wrapper-Funktion zu verwenden, falls vorhanden
|
ergebnis = self.pruefmanager.verarbeite(ergebnis)
|
||||||
try:
|
|
||||||
# qgisui_wrapper kann eine spezialisierte Dialogfunktion bereitstellen
|
|
||||||
from sn_basis.functions import qgisui_wrapper as qgisui
|
|
||||||
ask_fn = getattr(qgisui, "ask_overwrite_append_cancel", None)
|
|
||||||
if callable(ask_fn):
|
|
||||||
# Die Wrapper-Funktion soll genau die drei Strings zurückgeben
|
|
||||||
choice = ask_fn(layer_name)
|
|
||||||
if choice in ("overwrite", "append", "cancel"):
|
|
||||||
return choice
|
|
||||||
except Exception:
|
|
||||||
# Falls Import/Wrapper fehlschlägt, weiter zum Qt-Fallback
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Fallback: direkte Qt-Dialoge über qt_wrapper (wenn verfügbar)
|
if not ergebnis.ok:
|
||||||
try:
|
return "abbrechen"
|
||||||
from sn_basis.functions import qt_wrapper as qt
|
|
||||||
QMessageBox = getattr(qt, "QMessageBox", None)
|
|
||||||
if QMessageBox is not None:
|
|
||||||
# Erzeuge und konfiguriere Dialog
|
|
||||||
msg = QMessageBox()
|
|
||||||
msg.setWindowTitle("Layer bereits vorhanden")
|
|
||||||
msg.setText(f"Der Layer '{layer_name}' existiert bereits. Was möchten Sie tun?")
|
|
||||||
overwrite_btn = msg.addButton("Überschreiben", QMessageBox.AcceptRole)
|
|
||||||
append_btn = msg.addButton("Anhängen", QMessageBox.AcceptRole)
|
|
||||||
cancel_btn = msg.addButton("Abbrechen", QMessageBox.RejectRole)
|
|
||||||
msg.setDefaultButton(overwrite_btn)
|
|
||||||
# Blockierend anzeigen
|
|
||||||
msg.exec_()
|
|
||||||
clicked = msg.clickedButton()
|
|
||||||
if clicked == overwrite_btn:
|
|
||||||
return "overwrite"
|
|
||||||
if clicked == append_btn:
|
|
||||||
return "append"
|
|
||||||
return "cancel"
|
|
||||||
except Exception:
|
|
||||||
# Qt nicht verfügbar oder Fehler beim Dialogaufbau
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Headless / Mock: gib Default zurück
|
return "verwenden" if ergebnis.aktion == "ok" else "ersetzen"
|
||||||
return default
|
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ class Linkpruefer:
|
|||||||
aktion="leer",
|
aktion="leer",
|
||||||
kontext=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
#evtl. Pfad-Objekte in string umwandeln
|
||||||
|
eingabe = str(eingabe)
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 1. Fall: URL
|
# 1. Fall: URL
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
|
"""
|
||||||
|
sn_basis/modules/pruef_ergebnis.py
|
||||||
|
|
||||||
|
Erweitertes Ergebnisobjekt für Dateiprüfungen mit Verfahrens-DB-spezifischen Aktionen.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Optional, Literal
|
from typing import Any, Optional, Literal
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Erweiterte PruefAktionen für Verfahrens-DB-Workflow
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
# Erweitertes Literal mit allen erlaubten Aktionen (PruefAktion)
|
|
||||||
PruefAktion = Literal[
|
PruefAktion = Literal[
|
||||||
|
# Basis-Aktionen (bestehend)
|
||||||
"ok",
|
"ok",
|
||||||
"leer",
|
"leer",
|
||||||
"leereingabe_erlaubt",
|
"leereingabe_erlaubt",
|
||||||
@@ -16,6 +28,8 @@ PruefAktion = Literal[
|
|||||||
"pfad_nicht_gefunden",
|
"pfad_nicht_gefunden",
|
||||||
"url_nicht_erreichbar",
|
"url_nicht_erreichbar",
|
||||||
"netzwerkfehler",
|
"netzwerkfehler",
|
||||||
|
|
||||||
|
# Layer-spezifisch
|
||||||
"layer_nicht_gefunden",
|
"layer_nicht_gefunden",
|
||||||
"layer_unsichtbar",
|
"layer_unsichtbar",
|
||||||
"falscher_geotyp",
|
"falscher_geotyp",
|
||||||
@@ -25,15 +39,35 @@ PruefAktion = Literal[
|
|||||||
"felder_fehlen",
|
"felder_fehlen",
|
||||||
"datenquelle_unerwartet",
|
"datenquelle_unerwartet",
|
||||||
"layer_nicht_editierbar",
|
"layer_nicht_editierbar",
|
||||||
|
|
||||||
|
# Dateiendung/Format
|
||||||
"falsche_endung",
|
"falsche_endung",
|
||||||
"pflichtfelder_fehlen",
|
"pflichtfelder_fehlen",
|
||||||
# Excel / Import-spezifische Aktionen
|
"unbekannter_dateityp",
|
||||||
|
"Datenbank",
|
||||||
|
"dienst",
|
||||||
|
"excel",
|
||||||
|
"unbekannte_quelle",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Excel/Import
|
||||||
"kein_header",
|
"kein_header",
|
||||||
"kein_arbeitsblatt",
|
"kein_arbeitsblatt",
|
||||||
"read_error",
|
"read_error",
|
||||||
"open_error",
|
"open_error",
|
||||||
"datenabruf",
|
"datenabruf",
|
||||||
# Generische Prüf-/Speicher-Aktionen
|
|
||||||
|
|
||||||
|
|
||||||
|
# 🆕 VERFAHRENS-DB SPEZIFISCH (deine Anforderungen 2.d, 2.e)
|
||||||
|
"datei_wird_erzeugt", # 2.d: Pfad gültig, Datei fehlt → weiter
|
||||||
|
"datei_existiert", # Datei vorhanden → Layer-Entscheidung
|
||||||
|
"datei_existiert_ueberschreiben", # 2.e: Nutzer wählt "Überschreiben"
|
||||||
|
"datei_existiert_anhaengen", # 2.e: Nutzer wählt "Anhängen"
|
||||||
|
"datei_existiert_ueberspringen", # 2.e: Nutzer wählt "Überspringen"
|
||||||
|
|
||||||
|
# Generisch
|
||||||
"pruefe_exception",
|
"pruefe_exception",
|
||||||
"save_exception",
|
"save_exception",
|
||||||
"save_not_implemented",
|
"save_not_implemented",
|
||||||
@@ -42,22 +76,113 @@ PruefAktion = Literal[
|
|||||||
"needs_user_action",
|
"needs_user_action",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class pruef_ergebnis:
|
class pruef_ergebnis:
|
||||||
"""
|
"""
|
||||||
Einheitliches Ergebnisobjekt für Prüfer.
|
Einheitliches Ergebnisobjekt für Prüfer im Verfahrens-DB-Workflow.
|
||||||
- ok: True wenn Prüfung bestanden
|
|
||||||
- meldung: menschenlesbare Meldung
|
Attributes
|
||||||
- aktion: maschinenlesbarer Aktionscode (PruefAktion)
|
----------
|
||||||
- kontext: optionaler Zusatzkontext (z. B. Pfad, Layer-Objekt)
|
ok : bool
|
||||||
|
True wenn Prüfung bestanden und Pipeline fortgesetzt werden kann.
|
||||||
|
False signalisiert Fehler oder Nutzerentscheidung erforderlich.
|
||||||
|
meldung : Optional[str], optional
|
||||||
|
Menschenlesbare Meldung für UI-Dialoge (BY: Pruefmanager).
|
||||||
|
aktion : Optional[PruefAktion], optional
|
||||||
|
Maschinenlesbarer Aktionscode für nachfolgende Pipeline-Schritte.
|
||||||
|
kontext : Optional[Any], optional
|
||||||
|
Zusatzkontext: meist `pathlib.Path` für Dateipfade oder Layer-Objekte.
|
||||||
|
|
||||||
|
Verfahrens-DB-spezifische Aktionen:
|
||||||
|
|
||||||
|
+-----------------------------+-------------------------------------------------+
|
||||||
|
| Aktion | Bedeutung |
|
||||||
|
+=============================+=================================================+
|
||||||
|
| ``datei_wird_erzeugt`` | 2.d: Neues GPKG wird angelegt (Pfad gültig) |
|
||||||
|
+-----------------------------+-------------------------------------------------+
|
||||||
|
| ``datei_existiert`` | Datei vorhanden → Layer-Überschreibung prüfen |
|
||||||
|
+-----------------------------+-------------------------------------------------+
|
||||||
|
| ``datei_existiert_*`` | 2.e: Nutzerentscheidung für bestehende Datei |
|
||||||
|
+-----------------------------+-------------------------------------------------+
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ok: bool
|
ok: bool
|
||||||
meldung: Optional[str] = None
|
meldung: Optional[str] = None
|
||||||
aktion: Optional[PruefAktion] = None
|
aktion: Optional[PruefAktion] = None
|
||||||
kontext: Optional[Any] = None
|
kontext: Optional[Any] = None
|
||||||
|
|
||||||
def __init__(self, ok: bool, meldung: Optional[str] = None, aktion: Optional[PruefAktion] = None, kontext: Optional[Any] = None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
ok: bool,
|
||||||
|
meldung: Optional[str] = None,
|
||||||
|
aktion: Optional[PruefAktion] = None,
|
||||||
|
kontext: Optional[Any] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Erstellt ein neues Prüfergebnis.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ok : bool
|
||||||
|
True für "weiter mit Pipeline", False für "Entscheidung/Fehler".
|
||||||
|
meldung : Optional[str]
|
||||||
|
UI-Text für Nutzerdialoge.
|
||||||
|
aktion : Optional[PruefAktion]
|
||||||
|
Maschinenaktion für nachfolgende Verarbeitung.
|
||||||
|
kontext : Optional[Any]
|
||||||
|
Typischerweise `pathlib.Path` (Dateipfad) oder `QgsVectorLayer`.
|
||||||
|
"""
|
||||||
self.ok = ok
|
self.ok = ok
|
||||||
self.meldung = meldung
|
self.meldung = meldung
|
||||||
self.aktion = aktion
|
self.aktion = aktion
|
||||||
self.kontext = kontext
|
self.kontext = kontext
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ist_verfahrens_db_aktion(self) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft, ob es sich um eine Verfahrens-DB-spezifische Aktion handelt.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
True für ``datei_wird_erzeugt`` oder ``datei_existiert*``.
|
||||||
|
"""
|
||||||
|
return self.aktion in {
|
||||||
|
"datei_wird_erzeugt",
|
||||||
|
"datei_existiert",
|
||||||
|
"datei_existiert_ueberschreiben",
|
||||||
|
"datei_existiert_anhaengen",
|
||||||
|
"datei_existiert_ueberspringen",
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dateipfad(self) -> Optional[Path]:
|
||||||
|
"""
|
||||||
|
Extrahiert den Dateipfad aus dem Kontext (falls vorhanden).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Optional[Path]
|
||||||
|
`Path`-Objekt oder None.
|
||||||
|
"""
|
||||||
|
if isinstance(self.kontext, Path):
|
||||||
|
return self.kontext
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def erlaubte_persistierung(self) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft, ob die Pipeline Daten persistieren darf.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
True für ``datei_wird_erzeugt``, ``datei_existiert_ueberschreiben``,
|
||||||
|
``datei_existiert_anhaengen``.
|
||||||
|
"""
|
||||||
|
return self.aktion in {
|
||||||
|
"datei_wird_erzeugt",
|
||||||
|
"datei_existiert_ueberschreiben",
|
||||||
|
"datei_existiert_anhaengen",
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ Prüft ausschließlich, ob ein Stilpfad gültig ist.
|
|||||||
Die Anwendung erfolgt später über eine Aktion.
|
Die Anwendung erfolgt später über eine Aktion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
import os
|
||||||
|
|
||||||
from sn_basis.functions import file_exists
|
from sn_basis.functions.os_wrapper import is_absolute_path
|
||||||
|
from sn_basis.functions.sys_wrapper import get_plugin_root, file_exists, join_path
|
||||||
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
|
||||||
|
|
||||||
|
|
||||||
@@ -40,7 +41,11 @@ class Stilpruefer:
|
|||||||
kontext=None,
|
kontext=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
pfad = Path(stil_pfad)
|
pfad = str(stil_pfad)
|
||||||
|
|
||||||
|
if not is_absolute_path(pfad):
|
||||||
|
plugin_root = get_plugin_root()
|
||||||
|
pfad = str(join_path(plugin_root, "sn_plan41", "assets", pfad))
|
||||||
|
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 2. Datei existiert nicht
|
# 2. Datei existiert nicht
|
||||||
@@ -56,7 +61,7 @@ class Stilpruefer:
|
|||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
# 3. Falsche Endung
|
# 3. Falsche Endung
|
||||||
# -----------------------------------------------------
|
# -----------------------------------------------------
|
||||||
if pfad.suffix.lower() != ".qml":
|
if os.path.splitext(pfad)[1].lower() != ".qml":
|
||||||
return pruef_ergebnis(
|
return pruef_ergebnis(
|
||||||
ok=False,
|
ok=False,
|
||||||
meldung="Die Stil-Datei muss die Endung '.qml' haben.",
|
meldung="Die Stil-Datei muss die Endung '.qml' haben.",
|
||||||
|
|||||||
Reference in New Issue
Block a user