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>
16 KiB
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
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 ruftsn_basisdasinitGui()nach). initGui()holtplugins.get("sn_basis"); ohnebasis.uinichts tun (QGIS lädt Plugins in nicht garantierter Reihenfolge;sn_basisruft 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 einsn_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; Klassenattributtabs = [TabKlasse, ...]. - Tab-Klasse:
QWidget-Unterklasse mit Klassenattributtab_title; KonstruktorTab(parent). Optionalset_services(pruefmanager=..., data_grabber=...)(wird von Fachplugin-Docks aufgerufen) undcleanup()(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.closeEventsetzt sie zurück.- Scroll-Bereich je Tab (Basis #104):
BaseDockWidget.setWidget(widget)packt jeden Tab einesQTabWidgetin einenQScrollArea(widgetResizable, ohne Rahmen; Funktionbase_dockwidget.tabs_mit_scrollbereich). Das gilt auch für Fachplugins, die ihre Tabs in_init_tabs_with_services()selbst aufbauen und persetWidgeteinsetzen. 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 dasQTabWidget. Tabs vorsetWidgetvollstä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 inmap_item.crs(). Wer eine Ausdehnung weitergibt, rechnet sie mitqgiscore_wrapper.transformiere_rechteck(rechteck, quell_crs, ziel_crs)um. - Zoom auf einen Layer:
qgisui_wrapper.zoom_auf_layer(layer, canvas=None)– niecanvas.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_layoutnehmen es als Parametercrsund 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.PruefAktionist einLiteralmit Codes wiedatei_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). Mitui_modus != "qgis"(Tests) werden Standardantworten ohne Dialog geliefert.- Fachplugins erhalten eine
Pruefmanager- und eineDataGrabber-Instanz ausmain.py(Konstruktor) und reichen sie an Tabs weiter.
5. Datenpipeline Plan41 (Fachdaten aus Linkliste)
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, siehefallstricke.md. - Die einzige Rückwärtsabhängigkeit
sn_basis → sn_plan41ist der Lazy-Import vonListenauswerterinDataGrabber._process_excel_source(seit PR #57 abgesichert, damitsn_verfahrensgebietohnesn_plan41lädt).
6. Verfahrensgebiet-Abläufe (Fachplugin)
- Zentrale Kennung des Verfahrens: Projektvariable
verfahrensnummer(VKZ) und/oderlandkreis_kennzahl. - ALKIS-Quellen: vereinfachtes ALKIS (
.../public_alkis/vereinf/wfs) für Gemarkungen/BROR, ALKIS-SF (.../public_alkis/sf/wfs) für Flurstücke undPunktortTA. - 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 HeaderX-API-Key. - Laden: Verfahren (VKZ) → Plan41-GeoJSON → Memory-Layer je Geometriefamilie (
vln_layer_manager). Upload: alle Teillayer → eine FeatureCollection,PUTersetzt 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.pyerledigt 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
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-<kanal>.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
unstablebzw.testingsetzen. - Paketname im ZIP kommt aus der Gitea-Repo-Variable
ZIP_FOLDER(z. B.sn_basis). metadata.txtim Repo ist nur ein Abbild des letzten Builds; gepflegt wird.gitea/templates/metadata.template.- Release-Body = Block über dem ersten
---inchangelog.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 inSTATUS.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-Eigenschaftsn_basemap_legende = 1trä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>undskipMemoryLayersCheck = 1. Code, der überQgsProject.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 mitpython modules/basemap_katalog.py; gehört zur Style-Versionbm_web_colv2. - Stilvorlagen: lokal unter
<QGIS-Profil>/sn_basis/basemap_stile/, Standard inQSettingssn_basis/basemap_standardstil. Die Begleit-JSON trägt seit #100 das Feldthema(Schlüssel ausbasemap_themen.THEMEN; fehlt bei älteren Vorlagen). Austauschformat seit #101: ZIP mit genau einer<name>.qmlund 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 QuellparameterstyleUrl. „Schon vorhanden“ heißt: ein Layer mit identischer Kachel-URL (Quellparameterurl). - Layer-Eigenschaften des Basemap-Layers (#100):
sn_basemap_thema(Schlüssel des zuletzt geladenen Themas) undsn_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_themastellt die Quelle persetDataSourceum und lädtloadDefaultStyle(); 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 oderNonebei 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 oderNone.
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).