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

16 KiB
Raw Blame History

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 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)

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

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).