Files
Projektwissen/docs/architektur/schnittstellen.md
T
DanielandClaude Opus 5.5 1d3c98be11 Export und Import der Basemap-Stilvorlagen (Basis #101) dokumentiert
Konzept 10 Abschnitt 5.3, Austauschformat in schnittstellen.md,
Modulkarte und Testkatalog mit den neuen Tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 09:35:46 +02:00

202 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Schnittstellen und Verträge
Lebende Referenz der **impliziten Verträge** zwischen den Komponenten. Wer eine dieser Stellen ändert, ändert eine
Schnittstelle und muss alle Nutzer mit anpassen. Stand 02.09.2026.
## 1. Plugin-Lebenszyklus: sn_basis ↔ Fachplugin
```mermaid
sequenceDiagram
participant QGIS
participant B as sn_basis.BasisPlugin
participant F as Fachplugin (z. B. Verfahrensgebiet)
QGIS->>B: classFactory(iface) / initGui()
B->>B: Navigation().init_ui() (Menü + Toolbar "LNO Sachsen")
B->>B: setup_verfahrensgebiet_listener()
loop für jedes Plugin in qgis.utils.plugins mit Namen sn_* (außer sn_basis)
B->>F: initGui()
F->>B: basis.ui.add_action(name, run, tooltip, priority)
end
B->>B: finalize_menu_and_toolbar() (sortiert nach priority)
QGIS->>F: Nutzer klickt Action → run()
F->>F: DockWidget(...) mit objectName "sn_dock_<name>"
F->>B: DockManager.show(dock) (entfernt andere sn_dock_*)
F->>B: basis.ui.set_active_plugin(action)
QGIS->>F: unload() → removeDockWidget, basis.ui.remove_action(action)
```
Vertrag für ein Fachplugin (Muster: `Plugin_SN_Widmung/main.py`):
- Paketname beginnt mit `sn_` (nur dann ruft `sn_basis` das `initGui()` nach).
- `initGui()` holt `plugins.get("sn_basis")`; ohne `basis.ui` nichts tun (QGIS lädt Plugins in nicht garantierter Reihenfolge; `sn_basis` ruft später nach).
- `add_action(text, callback, tooltip="", priority=100)` → `QAction` (checkable, in exklusiver Gruppe). Vergebene Prioritäten: Verfahrensgebiet 10, Plan41 20, Widmung 30.
- Dock-Objektname `sn_dock_<pluginname klein>`; nur ein `sn_dock_*` ist gleichzeitig sichtbar.
- `unload()` muss Dock entfernen und Action abmelden, sonst bleiben Reste in Menü/Toolbar.
## 2. DockWidget und Tabs
- Fachplugin-Dock erbt von `sn_basis.ui.base_dockwidget.BaseDockWidget`; Klassenattribut `tabs = [TabKlasse, ...]`.
- Tab-Klasse: `QWidget`-Unterklasse mit Klassenattribut `tab_title`; Konstruktor `Tab(parent)`.
Optional `set_services(pruefmanager=..., data_grabber=...)` (wird von Fachplugin-Docks aufgerufen) und `cleanup()` (wird beim Schließen des Docks aufgerufen – Signale trennen!).
- Gemeinsame Tabs aus `sn_basis`: `ui.tabs.print_tab.PrintTab` (Druck), `ui.tabs.settings_tab.SettingsTab` (Projekteigenschaften).
- `dock.action` = Referenz auf die Toolbar-Action; `BaseDockWidget.closeEvent` setzt sie zurück.
- **Scroll-Bereich je Tab (Basis #104):** `BaseDockWidget.setWidget(widget)` packt jeden Tab eines `QTabWidget` in einen
`QScrollArea` (`widgetResizable`, ohne Rahmen; Funktion `base_dockwidget.tabs_mit_scrollbereich`). Das gilt auch für
Fachplugins, die ihre Tabs in `_init_tabs_with_services()` selbst aufbauen und per `setWidget` einsetzen. Folge:
`dock.widget().widget(i)` liefert den Scroll-Bereich, die Tab-Instanz ist `….widget(i).widget()`; das Eltern-Widget
eines Tabs ist der Viewport des Scroll-Bereichs, nicht mehr das `QTabWidget`. Tabs vor `setWidget` vollständig hinzufügen.
## 2a. Koordinatenbezugssysteme (Basis #99)
- **Ausdehnungen stehen im KBS ihrer Quelle:** `layer.extent()` im Layer-KBS, `canvas.extent()` im KBS der
Kartenansicht (`canvas.mapSettings().destinationCrs()`), Layoutkarten in `map_item.crs()`. Wer eine Ausdehnung
weitergibt, rechnet sie mit `qgiscore_wrapper.transformiere_rechteck(rechteck, quell_crs, ziel_crs)` um.
- **Zoom auf einen Layer:** `qgisui_wrapper.zoom_auf_layer(layer, canvas=None)` – nie `canvas.setExtent(layer.extent())`.
- **Druck-KBS:** Die Hauptkarte der Layouts (§ 38/§ 41, Baufreigabe) steht im KBS des Verfahrensgebiet-Layers, ohne
diesen im Projekt-KBS (`PrintLogic._druck_crs`); `create_single_page_layout`/`create_baufreigabe_layout` nehmen es
als Parameter `crs` und setzen es vor der Ausdehnung. Alle Druckbereiche werden in dieses KBS umgerechnet.
- **Druckrechteck:** Der Layer „Druckrechteck“ hat das KBS des Verfahrensgebiets; das in der Kartenansicht
gezeichnete Rechteck wird vor dem Speichern umgerechnet. `druckrechteck_xmin/…` stehen im KBS dieses Layers.
## 3. Projektvariablen (gemeinsamer Zustand im QGIS-Projekt)
Zugriff nur über `sn_basis.functions.variable_wrapper.get_variable(key, scope="project")` / `set_variable(...)`.
Der Wrapper setzt das Präfix `sn_` selbst; Schlüssel **ohne** Präfix übergeben.
| Schlüssel (ohne `sn_`) | Gesetzt von | Gelesen von | Bedeutung |
|---|---|---|---|
| `amt`, `behoerde`, `landkreis_user`, `sachgebiet` | SettingsTab | Druck (Schriftfeld) | benutzerspezifische Angaben |
| `bezeichnung`, `name`, `verfahrensnummer`, `gemeinden`, `landkreise_proj`, `landkreis_kennzahl` | SettingsTab | Verfahrensgebiet (ALKIS-Suche), Druck | Verfahrensangaben; `verfahrensnummer` = VKZ |
| `verfahrensgebiet_layer` | `verfahrensgebiet_manager`, WorkingTab | alle Fachplugins, Druck | Layer-ID des aktuellen Verfahrensgebiets |
| `verfahrens_db`, `tab_a_verfahrens_db` | Plan41 Tab A | Plan41 | Pfad des Verfahrens-GeoPackages |
| `linkliste`, `tab_a_linkliste` | Plan41 Tab A | Plan41 | Pfad der Linkliste (XLSX) |
| `tab_a_layer_id`, `Raumfilter` | Plan41 Tab A | Plan41 | Raumfilter-Einstellung |
| `druckrechteck_xmin/xmax/ymin/ymax` | PrintTab/PrintLogic | Druck | gespeicherter Druckbereich |
| `basemap_auswertebereich` | Basemap-Bereich im Druck-Tab | `basemap_legende` | `verfahrensgebiet` (Standard) oder `druckbereich` (Basis #88) |
| `basemap_legende_erzeugen` | Basemap-Bereich | `print_logic` | `1`/`0`, Standard `1`: Basemap-Legende beim Erstellen einer §-38/§-41-Vorlage |
| `basemap_legende_auswahl` | `basemap_legende`, Stilvorlage laden | Auswahldialog | JSON `{"ausgeblendet": [...], "eingeblendet": [...]}` mit Eintragsschlüsseln des Legendenkatalogs |
| `sn_abdeckungslayer` ⚠ | PrintTab/PrintLogic | Druck (Atlas) | Atlas-Abdeckungslayer. **Schlüssel enthält bereits `sn_` → tatsächliche Variable `sn_sn_abdeckungslayer`**; funktioniert, ist aber inkonsistent |
| `verfahrensgebietslayer` ⚠ | `print_logic.py` | `print_logic.py` | **Zweite Schreibweise neben `verfahrensgebiet_layer`** – vermutlich Altlast, beim Refactoring zusammenführen |
Benutzer-/Maschinendaten außerhalb des Projekts: `QSettings` in `sn_plan41` (`mail`, `api_key` der VLN-API).
## 4. Prüf- und Entscheidungsmodell
- `pruef_ergebnis(ok: bool, meldung, aktion: PruefAktion, kontext)` ist das einheitliche Rückgabeobjekt aller Prüfer (`Dateipruefer`, `Layerpruefer`, `Linkpruefer`, `Stilpruefer`, `Datenbankpruefer`) und vieler Fachfunktionen.
- `PruefAktion` ist ein `Literal` mit Codes wie `datei_nicht_gefunden`, `layer_leer`, `datei_existiert_ueberschreiben`, `verfahrensgebiet_ergaenzen`. Neue Codes dort ergänzen, nicht als freie Strings verwenden.
- `Pruefmanager(ui_modus="qgis")`: `verarbeite(ergebnis)` entscheidet je Aktion (Dialog, Meldung, Standardwert). Mit `ui_modus != "qgis"` (Tests) werden Standardantworten ohne Dialog geliefert.
- Fachplugins erhalten **eine** `Pruefmanager`- und **eine** `DataGrabber`-Instanz aus `main.py` (Konstruktor) und reichen sie an Tabs weiter.
## 5. Datenpipeline Plan41 (Fachdaten aus Linkliste)
```mermaid
flowchart LR
XLSX[assets/Linkliste.xlsx] --> DG[DataGrabber.run]
DG -->|Excel| LA[sn_plan41.Listenauswerter.validate_rows]
LA --> DA[Datenabruf.datenabruf]
VG[(Verfahrensgebiet / Puffer)] --> DA
DA --> LL[LayerLoader: create_layer, filter_by_layer/extent, apply_style]
LL --> DS[Datenschreiber.schreibe_Daten → GeoPackage]
DS --> P[Datenschreiber.lade_Layer + schreibe_log HTML]
```
- Erwartetes Spaltenschema der XLSX: `ident, gruppe, kartenebene, inhalt, link, provider, stildatei`. `provider` ∈ WFS, REST (ArcGIS), OGR.
- Das zentrale Repository `Linkliste` (`linkliste.csv`: `id;gruppe;ebene;inhalt;url;provider;stil`) ist **nicht** angebunden → zwei Wahrheiten, siehe `fallstricke.md`.
- Die einzige Rückwärtsabhängigkeit `sn_basis → sn_plan41` ist der Lazy-Import von `Listenauswerter` in `DataGrabber._process_excel_source` (seit PR #57 abgesichert, damit `sn_verfahrensgebiet` ohne `sn_plan41` lädt).
## 6. Verfahrensgebiet-Abläufe (Fachplugin)
- Zentrale Kennung des Verfahrens: Projektvariable `verfahrensnummer` (VKZ) und/oder `landkreis_kennzahl`.
- ALKIS-Quellen: vereinfachtes ALKIS (`.../public_alkis/vereinf/wfs`) für Gemarkungen/BROR, ALKIS-SF (`.../public_alkis/sf/wfs`) für Flurstücke und `PunktortTA`.
- Layer-Namenskonventionen (String-Konstanten in den Modulen): `BROR_LAYER_NAME`, `FLST_LAYER_NAME`, `JOINED_LAYER_NAME`, „Verfahrensgebiet“, „Neuordnungsgebiet (NoBIS)“, „Neuvermessungsgebiet (NoBIS)“. Andere Module finden Layer **über den Namen** – Umbenennungen sind Schnittstellenänderungen.
- Ablaufdokumentation mit Diagrammen: `Plugin_SN_Verfahrensgebiet/docs/`.
## 7. VLN-Karten-API (Plan41)
- Basis-URL `https://api.flurneuordnung-sachsen.de/v2`; Login per E-Mail/Passwort, danach Header `X-API-Key`.
- Laden: Verfahren (VKZ) → Plan41-GeoJSON → Memory-Layer je Geometriefamilie (`vln_layer_manager`). Upload: alle Teillayer → eine FeatureCollection, `PUT` ersetzt den Serverbestand der VKZ vollständig.
- Arbeits-CRS EPSG:25833.
## 8. Mock-Modus (Tests ohne QGIS)
- Jeder Wrapper versucht den Import von `qgis`/`PyQt6`/`PyQt5`; schlägt das fehl, definiert er `_Mock*`-Klassen. Kennzeichen: `qgiscore_wrapper.QGIS_AVAILABLE`, `qt_wrapper.QT_VERSION == 0`.
- Tests importieren Module über den Paketnamen (`from sn_basis...`), deshalb müssen die Repos unter ihrem Paketnamen erreichbar sein → `scripts/run_tests.py` erledigt das über ein temporäres Verzeichnis.
- Tests, die echtes Layer- oder Netzwerkverhalten brauchen, laufen nur in QGIS (`tests/test_qgis.bat`, Windows).
## 9. Release-Vertrag
```mermaid
flowchart LR
T["Tag v26.9.1-u auf Plugin-Repo"] --> W1["Plugin-Workflow release.yaml"]
W1 -->|"workflow_dispatch + Payload"| W2["Repository, Branch hidden/workflows"]
W2 --> C["checkout Branch = Kanal<br/>(unstable / testing / stable)"]
C --> M["metadata.template → metadata.txt:<br/>version, experimental, repository-URL"]
M --> Z["rsync ohne .git .gitea .gitignore __pycache__ → ZIP_FOLDER.zip"]
Z --> R["Gitea-Release + Asset"]
R --> X["plugins-&lt;kanal&gt;.xml aktualisieren"]
```
- Tagformat `v<JJ.M.N>-u` (unstable), `-t` (testing), ohne Suffix (stable). Version = Jahr.Monat.Laufnummer.
- **Gebaut wird der Kopf des Kanal-Branches, nicht der getaggte Commit.** Tag also nur auf dem aktuellen Kopf von `unstable` bzw. `testing` setzen.
- Paketname im ZIP kommt aus der Gitea-Repo-Variable `ZIP_FOLDER` (z. B. `sn_basis`).
- `metadata.txt` im Repo ist nur ein Abbild des letzten Builds; gepflegt wird `.gitea/templates/metadata.template`.
- Release-Body = Block über dem ersten `---` in `changelog.txt`.
- Neue Workspace-Dateien (`AGENTS.md`, `CLAUDE.md`, `.agents/`, `.claude/`, `docs/` der Repos) landen derzeit im ZIP → Excludes im zentralen Workflow ergänzen (offener Punkt in `STATUS.md`).
## 10. Linkliste-Repository
`linkliste.csv` (Semikolon, Spalten `id;gruppe;ebene;inhalt;url;provider;stil`) → Gitea Action → `linkliste.xml/json/yaml` auf `main`. Die Plan41-Laufzeit nutzt stattdessen die mitgelieferte `assets/Linkliste.xlsx` mit abweichenden Spaltennamen.
## 11. Basemap-Legende im Druck (Basis #88)
- **Welche Layouts:** `print_logic.LAYOUTS_MIT_BASEMAP_LEGENDE` (heute §38, §41). Ein Layout nutzt die Funktion,
wenn es die Layout-Eigenschaft `sn_basemap_legende = 1` trägt (`basemap_legende_layout.markiere_layout`);
`PrintLayout.create_single_page_layout(..., basemap_legende=True)` setzt sie und erzeugt die Legende vor dem
Öffnen des Designers. Neue Layouts: in die Konstante aufnehmen bzw. das Kennzeichen setzen.
- **Layout-Elemente nach ID:** `Hauptkarte` (Bezugskarte), `Legende` (Hauptlegende, Spalte und Oberkante),
`Quellenangabe` (Unterkante der Basemap-Legende), `Legende Basemap` (das Basemap-Element selbst).
Umbenennen dieser IDs bricht die Platzierung.
- **Weitere Layout-Eigenschaften:** `sn_basemap_bereich`, `sn_basemap_massstab`, `sn_basemap_eintraege` (JSON),
`sn_basemap_hilfslayer` (IDs der Hilfslayer, werden beim Aktualisieren ersetzt).
- **Hilfslayer:** leere Speicherlayer, nicht im Layerbaum, Eigenschaften `sn_basemap_legende = <Layoutname>` und
`skipMemoryLayersCheck = 1`. Code, der über `QgsProject.mapLayers()` iteriert, sieht sie.
- **Basemap-Layer-Erkennung:** Vektorkachel-Layer, Quelle enthält `gdz_basemapde_vektor`; Name „basemap.de“ nur
als Rückfall.
- **Legendenkatalog:** `sn_basis/data/basemap_legende.json` (Style-ID → Eintrag), erzeugt/aktualisiert mit
`python modules/basemap_katalog.py`; gehört zur Style-Version `bm_web_col` v2.
- **Stilvorlagen:** lokal unter `<QGIS-Profil>/sn_basis/basemap_stile/`, Standard in `QSettings`
`sn_basis/basemap_standardstil`. Die Begleit-JSON trägt seit #100 das Feld `thema` (Schlüssel aus
`basemap_themen.THEMEN`; fehlt bei älteren Vorlagen). Austauschformat seit #101: ZIP mit genau einer
`<name>.qml` und optional `<name>.json`; der Import nimmt auch eine reine QML-Datei an.
- **Thema und Stil (Basis #100):** *Thema* = vom Dienst als Style-JSON gelieferte Darstellung (`farbe` →
`bm_web_col.json`, `grau` → `bm_web_gry.json`); *Stil* = lokale Anpassung (Stilvorlage). Alle Themen nutzen
dieselbe Kachel-URL `…/tiles/v2/bm_web_de_3857/{z}/{x}/{y}.pbf`; das Thema steht im Quellparameter `styleUrl`.
„Schon vorhanden“ heißt: ein Layer mit identischer Kachel-URL (Quellparameter `url`).
- **Layer-Eigenschaften des Basemap-Layers (#100):** `sn_basemap_thema` (Schlüssel des zuletzt geladenen Themas)
und `sn_basemap_pruefwert` (SHA-256 der QML von Symbologie und Beschriftung ohne QGIS-Version, gesetzt nach
Laden eines Themas oder Stils und nach Speichern als Stil). Fehlt der Prüfwert, gilt der Änderungsstand als
unbekannt und vor dem Themenwechsel wird nachgefragt.
- **Themenwechsel:** `basemap_themen.wechsle_thema` stellt die Quelle per `setDataSource` um und lädt
`loadDefaultStyle()`; Layer-ID, Position im Layerbaum und Bezüge in Layouts bleiben. Ziel ist der im Layerbaum
ausgewählte Basemap-Layer, sonst der erste des Projekts.
- **Neue öffentliche Funktion:** `dialog_wrapper.ask_mehrfachauswahl(titel, hinweis, eintraege, vorauswahl)` –
Liste mit Häkchen, Gruppen, Suche; liefert gewählte Schlüssel oder `None` bei Abbruch.
- **Neue öffentliche Funktionen (#100):** `dialog_wrapper.ask_auswahl(title, message, optionen, default, abbrechen)` –
Rückfrage mit frei benannten Knöpfen `[(schluessel, beschriftung), …]`, liefert den Schlüssel; Esc/Fehler →
`abbrechen`, Mock-Modus → `default`. `qgisui_wrapper.get_active_layer()` – ausgewählter Layer oder `None`.
## 12. Kartenrand im Standardlayout (Basis #93)
`print_kartenrand.ordne_kartenrand_an(layout)` läuft am Ende von `PrintLayout.create_single_page_layout` und findet
die Elemente **über ihre IDs**: `Hauptkarte`, `Legende`, `Legende Basemap`, `Quellenangabe`, `Kartenschild`,
`Kartenschild_Linie_1` … `_6` und die Schriftfeld-Beschriftungen (`Herausgeber` … `Aktenzeichen`, Liste
`UNTERE_ELEMENTE`). Wer im Layoutaufbau eine dieser IDs ändert oder ein neues Element unter die Legenden setzt,
muss die Liste anpassen. Die Seitengröße aus `print_logic` ist danach nur noch eine Mindestgröße: Breite und Höhe
können wachsen, die Karte bleibt bei 10 mm / 10 mm mit unveränderter Größe.
Legenden sind mindestens so breit wie das Kartenschild (180 mm, `MIN_SPALTENBREITE_MM`; die 210 mm in
`print_logic.STANDARD_LAYOUT_FIXBREITE_MM` sind der gesamte Kartenrand mit Abständen). Weil QGIS die Mindestgröße
nicht speichert, trägt das Layout die Eigenschaft `sn_legende_mindestbreite`; `sn_basis.main` verbindet beim Start
`setup_mindestbreiten_listener()` (readProject) und trennt ihn in `unload()` (Basis #96).