# 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_" 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_`; 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. ## 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
(unstable / testing / stable)"] C --> M["metadata.template → metadata.txt:
version, experimental, repository-URL"] M --> Z["rsync ohne .git .gitea .gitignore __pycache__ → ZIP_FOLDER.zip"] Z --> R["Gitea-Release + Asset"] R --> X["plugins-<kanal>.xml aktualisieren"] ``` - Tagformat `v-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 = ` 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 `/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). - **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).