Konzept Basemap-Legende und Basemap-Stilvorlagen im Druck (Basis #88)

Neue Konzeptseite docs/konzept/10_basemap_legende.md mit den abgestimmten
Festlegungen, technischen Grundlagen (selectByGeometry, Style-JSON bm_web_col),
Bausteinen, Tests, Umsetzungsschritten und Abnahmekriterien; in docs/README.md
verlinkt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 11:27:49 +02:00
co-authored by Claude Opus 5.5
parent a0bb40aeee
commit bd8b215ddd
2 changed files with 236 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
# Basemap-Legende und Basemap-Stilvorlagen im Druck
Stand 02.10.2026. Konzept zur Freigabe, Ticket **Plugin_SN_Basis #88** („Legende für basemap").
Betroffen ist ausschließlich **`sn_basis`** (Druck); `sn_plan41` und `sn_verfahrensgebiet` nutzen den
Druck-Tab aus `sn_basis` und bekommen die Funktion ohne eigene Änderung.
## 1. Ziel
Der Vektorkachel-Dienst basemap.de
(`https://sgx.geodatenzentrum.de/gdz_basemapde_vektor/tiles/v2/bm_web_de_3857/{z}/{x}/{y}.pbf`) hat keine
Legende, die nur die tatsächlich dargestellten Objekte zeigt. Für die Drucklayouts (§ 38, § 41) wird eine solche
Legende gebraucht:
1. Beim Anlegen einer Druckvorlage wird geprüft, ob ein Basemap-Layer im Projekt ist.
2. Die Objekte der Kacheln **im Auswertebereich** (Standard: Verfahrensgebiet) werden ausgewertet.
3. Ein Auswahldialog zeigt die gefundenen Kategorien; der Nutzer wählt ab, was nicht in die Legende soll.
4. Die gewählten Kategorien erscheinen mit der Signatur **aus dem Projektlayer** (Standard „Basemap.de - Farbe")
in der Legende – nicht mit der Signatur aus dem Signaturenkatalog, weil die Darstellung im Projekt abweichen kann.
Dazu kommt ein einklappbarer Bereich **„Basemap"** im Druck-Tab (Auswertebereich, Legende aktualisieren,
Stilvorlagen laden/speichern).
## 2. Festlegungen (mit Daniel abgestimmt, 02.10.2026)
| # | Frage | Festlegung |
|---|---|---|
| 1 | Für welche Layouts? | Funktion layoutunabhängig bauen; jedes Layout kann sie per Kennzeichen nutzen. **Vorerst aktiv für § 38 und § 41**, nicht für „Anlage zur Baufreigabe". |
| 2 | Auswertebereich | **Standard Verfahrensgebiet.** Umschaltbar auf den Druckbereich (Kartenausschnitt der Vorlage) im Basemap-Bereich des Druck-Tabs. Ohne Verfahrensgebiet: Rückfall auf den Druckbereich mit Hinweis. |
| 3 | Feinheit der Einträge | Brücken-, Tunnel- und Fahrbahnvarianten gehen im Grundeintrag auf („Kreisstraße"). „Im Bau" und „Außer Betrieb" werden eigene Einträge. Beschriftungen (`Name_*`, `Nummer_*`), Hausnummern und `Hintergrund` kommen nie in die Legende. |
| 4 | Basemap-Layer in der Hauptlegende | Der Layer selbst (heute nur Name ohne Symbol) wird ausgeblendet und durch die Gruppe „Basemap" ersetzt. |
| 5 | Nachträglich aktualisieren | Knopf „Basemap-Legende aktualisieren" im Basemap-Bereich, für bestehende Vorlagen. |
| 6 | Stilvorlagen | Darstellung des Basemap-Layers als **lokale Stilvorlage** speichern und wieder laden, **mehrere Varianten** möglich, eine davon als Standard markierbar. |
## 3. Technische Grundlagen (recherchiert 02.10.2026)
- **QGIS bietet für Vektorkachel-Layer keine Legende.** `QgsVectorTileLayer` setzt keine `QgsMapLayerLegend`
(Quellcode geprüft für 3.40, 3.44, master). Die Legendeneinträge müssen wir selbst erzeugen.
- **Auswertung über `QgsVectorTileLayer.selectByGeometry()`** (seit QGIS 3.28): holt die Kacheln zur Zoomstufe
des übergebenen Maßstabs (`QgsSelectionContext.setScale`), dekodiert sie, schneidet sie mit der Geometrie (im
Layer-KBS EPSG:3857) und liefert über `selectedFeatures()` Features mit den Zusatzfeldern `tile_layer` und
`tile_zoom`. Mit übergebenem `QgsRenderContext` werden nur Features geliefert, die der Renderer auch zeichnet.
`getRawTile()` ist in Python nicht verfügbar (`SIP_SKIP`). Der Aufruf **verändert die Auswahl des Layers**
– vorher sichern, danach wiederherstellen.
- **Kacheln enden bei Zoomstufe 15** (`bm_web_de_3857.json`). Druckmaßstäbe 1:1.000 bis 1:5.000 werten Stufe 15
aus. Eine Kachel der Stufe 15 deckt in Sachsen rund 0,6 km² ab → ein Verfahrensgebiet von 50 km² braucht
etwa 100 Kacheln. Bereits von der Kartenansicht geladene Kacheln kommen aus dem Netzwerkcache.
- **Der Signaturenkatalog ist die Style-JSON** `https://sgx.geodatenzentrum.de/gdz_basemapde_vektor/styles/bm_web_col.json`
(569 Style-Layer: 283 Linien, 210 Symbole, 66 Flächen, 7 Kreise, 3 Extrusionen; 29 Quell-Layer).
Style-IDs sind technische Namen (`Kontur_Kreisstr`, `Decker_Kreisstr`, `Bruecke_Kreisstr_getrennt_Decker` …),
**ein Legendeneintrag besteht aus mehreren Style-Layern** (Kreisstraße: 17). Lesbare Bezeichnungen liefern die
`klasse`-Werte der Filter (520 Werte über alle Quell-Layer).
- **Beim Import in QGIS wird jede Style-ID zum `styleName`** eines `QgsVectorTileBasicRendererStyle` (Layername,
Geometrietyp, Filterausdruck, Zoombereich, Symbol, aktiv/inaktiv). Beschriftungen landen in
`QgsVectorTileBasicLabeling`. Darüber verbinden wir Katalog und Projektlayer.
- **Zoomabhängige Strichbreiten** (Mapbox-`stops`) werden in QGIS zu datendefinierten Ausdrücken mit
`@vector_tile_zoom`. In der Legende gibt es diese Variable nicht → Werte für die Druck-Zoomstufe festschreiben
oder die Variable am Legendenelement setzen (Spike, Abschnitt 7).
Quellen: [Datenmodell](https://basemap.de/data/produkte/web_vektor/meta/bm_web_vektor_datenmodell.html),
[Signaturenkatalog](https://basemap.de/data/produkte/web_vektor/meta/bm_web_vektor_col_signaturenkatalog.html),
[PyQGIS QgsVectorTileLayer](https://qgis.org/pyqgis/3.44/core/QgsVectorTileLayer.html),
QGIS-Quellcode `src/core/vectortile/qgsvectortilelayer.cpp` (`final-3_40_0`, `final-3_44_0`).
## 4. Ablauf
```mermaid
flowchart TD
A[Vorlage erstellen<br/>oder Basemap-Legende aktualisieren] --> B{Layout nutzt<br/>Basemap-Legende?}
B -- nein --> Z[Layout wie bisher]
B -- ja --> C{Sichtbarer Basemap-Layer<br/>im Projekt?}
C -- nein --> Z
C -- ja --> D[Auswertebereich bestimmen<br/>Verfahrensgebiet / Druckbereich]
D --> E[Geometrie nach EPSG:3857,<br/>kachelweise selectByGeometry<br/>mit Fortschritt und Abbruch]
E --> F[Feature → aktive Renderer-Styles<br/>bei Druck-Zoomstufe]
F --> G[Styles → Legendeneinträge<br/>über Legendenkatalog]
G --> H[Auswahldialog mit Symbolvorschau<br/>Vorbelegung aus Projektvariable]
H --> I[Gruppe Basemap in der Legende<br/>zusammengesetzte Symbole]
I --> J[Auswahl und Einstellungen<br/>am Layout und im Projekt merken]
```
- **Layer-Erkennung:** Layertyp Vektorkachel **und** Quell-URL enthält `gdz_basemapde_vektor`; Name nur als
Rückfall (Layer werden sonst über den Namen gesucht, siehe `fallstricke.md`). Berücksichtigt werden nur Layer,
die in der Karte der Vorlage sichtbar sind (Thema bzw. aktuelle Sichtbarkeit). Mehrere Basemap-Layer
(z. B. Farbe und Grau): jeder bekommt eine eigene Gruppe.
- **Deaktivierte Styles** im Projektlayer erzeugen keine Legendeneinträge. Wer z. B. Hausnummern oder andere
Objekte über eine Stilvorlage ausschaltet, sieht sie automatisch auch nicht in der Legende.
- **Unbekannte Style-IDs** (eigene Ergänzungen im Projektlayer, neue Basemap-Version) erscheinen mit der
Bezeichnung „Quell-Layer: klasse" bzw. dem `styleName` in einer Gruppe „Sonstiges" und sind im Dialog
abgewählt vorbelegt.
## 5. Bausteine
### 5.1 `sn_basis` – neue und geänderte Dateien
| Datei | Inhalt |
|---|---|
| `data/basemap_legende.json` (neu) | **Legendenkatalog:** je Eintrag Schlüssel, Bezeichnung, Gruppe, Reihenfolge, `treffer_stile` (alle Style-IDs, deren Vorkommen den Eintrag auslösen), `symbol_stile` (Style-IDs, die zusammen das Legendensymbol bilden, z. B. `Kontur_Kreisstr` + `Decker_Kreisstr`), dazu eine Liste `nie_in_legende`. Kopf mit Version der Style-JSON. |
| `modules/basemap_legende.py` (neu, < 600 Zeilen) | Layer finden, Auswertebereich bestimmen, Kacheln auswerten, Styles zuordnen (reine Logik, testbar), Symbole zusammensetzen, Gruppe in eine Legende schreiben bzw. ersetzen. Keine direkten QGIS-/Qt-Importe. |
| `modules/basemap_stilvorlagen.py` (neu) | Stilvorlagen auflisten, speichern, laden, löschen, Standard merken. |
| `ui/widgets/basemap_bereich.py` (neu) | Einklappbarer Bereich „Basemap" für den Druck-Tab (Abschnitt 5.2). Eigene Datei, weil `print_tab.py` schon 709 Zeilen hat. |
| `ui/tabs/print_tab.py` | Bereich einbinden (wenige Zeilen), `cleanup()` weiterreichen. |
| `modules/print_logic.py` | Nach der Vorlagenbezeichnung Basemap-Legende ermitteln, wenn das Layout sie nutzt; Ergebnis an das Layout übergeben. Menge der Layouts mit Basemap-Legende als Konstante (`{KARTENNAME_38, KARTENNAME_41}`). |
| `modules/print_layout.py` | Neuer optionaler Parameter für die Basemap-Einträge; Layout-Kennzeichen setzen; Basemap-Layer-Knoten der Hauptlegende ausblenden. |
| `functions/dialog_wrapper.py` | Neuer allgemeiner Dialog `ask_mehrfachauswahl(titel, hinweis, eintraege, vorauswahl)`: Liste mit Häkchen, Symbolvorschau, Gruppen, Suche, „Alle auswählen". Mock: gibt die Vorauswahl zurück. |
| `functions/qgiscore_wrapper.py`, `qgisui_wrapper.py`, `qt_wrapper.py` | Fehlende Symbole mit Mock-Fallback: `QgsVectorTileLayer`, `QgsSelectionContext`, `QgsRenderContext`, `QgsExpression`, `QgsExpressionContext(Scope)`, `QgsCategorizedSymbolRenderer`, `QgsRendererCategory`, `QgsLayerTreeGroup`/`QgsLayerTreeLayer`, `QgsSymbolLayerUtils`, `QgsApplication.qgisSettingsDirPath`, `QgsCollapsibleGroupBox`. |
| `changelog.txt` | Eintrag oben (nächstes Release). |
**Layoutunabhängigkeit:** Ein Layout nutzt die Basemap-Legende, wenn es die Layout-Eigenschaft
`sn_basemap_legende = 1` trägt und ein Legendenelement mit bekannter ID hat (Standard `Legende`). Am Layout werden
außerdem Auswertebereich, Maßstab und gewählte Einträge gespeichert, damit „Aktualisieren" mit denselben
Einstellungen neu rechnen kann. Ein neues Layout schaltet die Funktion ein, indem es das Kennzeichen setzt bzw.
in die Konstante aufgenommen wird – ohne Änderung am Basemap-Modul.
### 5.2 Basemap-Bereich im Druck-Tab
Einklappbar (standardmäßig eingeklappt), unterhalb der bisherigen Einstellungen, oberhalb von „Vorlage erstellen".
Ohne Basemap-Layer im Projekt ist der Bereich deaktiviert und zeigt „Kein Basemap-Layer im Projekt".
```
▸ Basemap (Basemap.de - Farbe)
Legende
Auswertebereich [Verfahrensgebiet ▾] (Verfahrensgebiet | Druckbereich)
[x] Basemap-Legende beim Erstellen der Vorlage erzeugen
Vorlage [§41 Wegeplan ▾] [Basemap-Legende aktualisieren]
Stilvorlage
Variante [Ohne Hausnummern ★ ▾]
[Laden] [Speichern unter …] [Als Standard] [Löschen]
```
- „Vorlage" listet die Layouts des Projekts mit Kennzeichen `sn_basemap_legende`.
- „Aktualisieren" rechnet mit dem aktuell eingestellten Auswertebereich neu, zeigt den Auswahldialog
(vorbelegt mit der letzten Auswahl dieses Layouts) und ersetzt die Gruppe „Basemap" in der Legende.
### 5.3 Stilvorlagen (lokal, mehrere Varianten)
- **Inhalt einer Variante:** Darstellung des Basemap-Layers als QML (Kategorien Symbologie und Beschriftung,
also auch abgeschaltete Styles wie Hausnummern) und eine kleine JSON-Begleitdatei mit Anzeigename,
Erstellungsdatum, Quell-Stil (`bm_web_col` v2) und – optional – den in der Legende standardmäßig abgewählten
Einträgen.
- **Ablage:** lokal im QGIS-Profil, `<Profilordner>/sn_basis/basemap_stile/<name>.qml` + `<name>.json`.
Dateiname über `sanitize_bezeichner`. Der Standard wird in `QSettings` gemerkt (`sn_basis/basemap_standardstil`).
- **Laden:** Rückfrage vor dem Überschreiben der aktuellen Darstellung. Danach Prüfung, ob die `styleName`s der
Vorlage zum Layer passen (Farbe vs. Grau, neue Basemap-Version); bei Abweichung Hinweis mit Anzahl unbekannter
Styles. Laden geschieht **nur über den Knopf**, nicht automatisch beim Öffnen eines Projekts.
- **Speichern unter …:** Name abfragen, vorhandene Variante nur nach Rückfrage überschreiben.
- **Später möglich, nicht Teil dieses Tickets:** mitgelieferte Varianten im Plugin (`styles/basemap/`) und ein
gemeinsamer Ordner (Netzlaufwerk) für die ganze Behörde.
### 5.4 Werkzeug im Projektwissen
`scripts/basemap_katalog.py` (neu, nur Standardbibliothek): lädt `bm_web_col.json`, gruppiert Style-IDs nach
Quell-Layer und `klasse` (Präfixe/Suffixe wie `Kontur_`, `Decker_`, `Bruecke_`, `Tunnel_`, `_getrennt`,
`_Fahrbahnachse`, `_FSZ1` werden zusammengefasst), schlägt `symbol_stile` vor und schreibt einen Entwurf von
`basemap_legende.json`. Bestehende fachliche Nachbearbeitung (Bezeichnungen, Gruppen, Reihenfolge) bleibt bei
einem erneuten Lauf erhalten; neue oder entfallene Style-IDs werden gemeldet.
### 5.5 Neue Schnittstellen (in `schnittstellen.md` nachtragen)
| Art | Schlüssel | Bedeutung |
|---|---|---|
| Projektvariable | `basemap_auswertebereich` | `verfahrensgebiet` (Standard) oder `druckbereich` |
| Projektvariable | `basemap_legende_erzeugen` | `1`/`0`, Standard `1` |
| Projektvariable | `basemap_legende_ausgeblendet` | JSON-Liste der zuletzt abgewählten Eintragsschlüssel (Vorbelegung) |
| Layout-Eigenschaft | `sn_basemap_legende`, `sn_basemap_bereich`, `sn_basemap_massstab`, `sn_basemap_eintraege` | Kennzeichen und letzte Einstellungen je Layout |
| QSettings | `sn_basis/basemap_standardstil` | Name der Standard-Stilvorlage |
| Öffentliche Funktion | `dialog_wrapper.ask_mehrfachauswahl(...)` | neuer allgemeiner Dialog |
## 6. Tests (Mock-Modus, ohne Netzwerk)
Jede neue beobachtbare Funktion bekommt mindestens einen Test mit deutschem Docstring („Prüft, dass …").
| Bereich | Testfälle |
|---|---|
| Katalog | Laden und Schema; jede Style-ID höchstens einem Eintrag zugeordnet; `symbol_stile` ⊆ `treffer_stile`; Ausschlussliste greift |
| Zuordnung | Mehrere Styles → ein Eintrag; deaktivierte Styles ignoriert; Zoombereich berücksichtigt; Filterprüfung als übergebene Funktion; unbekannte Style-IDs → „Sonstiges", abgewählt vorbelegt; Reihenfolge nach Gruppe/Reihenfolge |
| Layer-Erkennung | Erkennung über Quell-URL; Name nur als Rückfall; unsichtbarer Layer ignoriert; kein Basemap-Layer → Druck unverändert |
| Auswertebereich | Verfahrensgebiet als Standard; Druckbereich bei Einstellung; ohne Verfahrensgebiet Rückfall mit Hinweis; Auswahl des Layers danach wiederhergestellt |
| Dialog | Mock gibt Vorauswahl zurück; Abbruch → keine Basemap-Gruppe, Vorlage trotzdem erzeugt |
| Layout | Kennzeichen nur bei § 38/§ 41; Gruppe „Basemap" wird beim Aktualisieren ersetzt, nicht verdoppelt; Basemap-Layer-Knoten ausgeblendet |
| Projektvariablen | Auswahl und Auswertebereich werden gespeichert und wieder gelesen |
| Stilvorlagen | Speichern, Auflisten, Laden, Löschen in temporärem Profilordner; Name über `sanitize_bezeichner`; Überschreiben nur nach Bestätigung; Standard in QSettings; fehlende Begleitdatei toleriert |
| Katalog-Skript | Gruppierung Kontur/Decker/Brücke zu einem Eintrag an einem Ausschnitt der Style-JSON (Testdaten im Repo, kein Download) |
**Nur in QGIS prüfbar** (Ergebnis im PR dokumentieren): echte Kachelauswertung, Laufzeit, Aussehen der Symbole
in der Legende, QML-Laden/Speichern am Vektorkachel-Layer, Einklappen des Bereichs unter Qt5 und Qt6.
## 7. Umsetzungsschritte
Ein Feature-Branch `feature/88-basemap-legende` in `Plugin_SN_Basis` von `unstable`, Commits je Schritt.
1. **Spike in QGIS** (Python-Konsole, echtes Verfahrensgebiet, QGIS 3.44 und 4.x). Ergebnis als ADR in
`docs/entscheidungen/`:
- Laufzeit und Kachelzahl von `selectByGeometry()` für ein großes Verfahrensgebiet; kachelweise Aufteilung.
- Auswertung der Style-Filter mit `@vector_tile_zoom`.
- **Darstellung in der Legende:** (a) leere Hilfslayer (Speicherlayer je Geometrietyp, kategorisiert, nicht im
Layerbaum) als Gruppe „Basemap" in der Hauptlegende – offen, ob `setLegendFilterByMapEnabled(True)` sie
wegfiltert; oder (b) eigenes Legendenelement „Basemap" ohne Kartenfilter direkt unter der Hauptlegende.
- Zusammengesetzte Symbole und festgeschriebene Strichbreiten in der Legende.
- `saveNamedStyle`/`loadNamedStyle` am Vektorkachel-Layer inklusive deaktivierter Styles und Beschriftung.
- `QgsCollapsibleGroupBox` unter Qt5/Qt6 verfügbar, sonst Muster des Atlas-Bereichs (Kontrollkästchen
blendet Inhalt ein).
2. **Legendenkatalog:** Skript, erster Entwurf, fachliche Durchsicht der Bezeichnungen und Gruppen.
3. **Auswertung und Zuordnung** (`basemap_legende.py`) mit Tests.
4. **Dialog** `ask_mehrfachauswahl` mit Test.
5. **Layout-Anbindung** in `print_logic`/`print_layout` für § 38/§ 41, Projektvariablen.
6. **Basemap-Bereich im Druck-Tab** mit Aktualisieren-Knopf.
7. **Stilvorlagen** (`basemap_stilvorlagen.py`, Teil des Bereichs).
8. `python scripts/run_tests.py sn_basis --baseline`, `python scripts/testkatalog.py`,
`python scripts/pruefe_umlaute.py Plugin_SN_Basis`, QGIS-Abnahme, `changelog.txt`, PR-Text, Übergabe:
`schnittstellen.md`, `modulkarte.md`, `fallstricke.md` (Katalog hängt an Stil-Version v2), `STATUS.md`.
## 8. Abnahmekriterien
- [ ] Beim Erstellen einer § 38- oder § 41-Vorlage mit sichtbarem Basemap-Layer erscheint der Auswahldialog mit
genau den Kategorien, die im Auswertebereich vorkommen und im Projektlayer aktiv sind.
- [ ] Die gewählten Kategorien stehen als Gruppe „Basemap" mit der Signatur des Projektlayers in der Legende;
zusammengesetzte Signaturen (Straßen) sind als ein Symbol dargestellt.
- [ ] Beschriftungen, Hausnummern und Hintergrund erscheinen nie.
- [ ] Ohne Basemap-Layer, bei Abbruch des Dialogs und bei „Anlage zur Baufreigabe" entsteht die Vorlage wie bisher.
- [ ] Auswertebereich ist im Basemap-Bereich auf Druckbereich umstellbar; die Einstellung bleibt im Projekt erhalten.
- [ ] „Basemap-Legende aktualisieren" ersetzt die Gruppe in einer bestehenden Vorlage, ohne sie zu verdoppeln.
- [ ] Stilvorlagen lassen sich unter eigenem Namen speichern, laden, löschen und als Standard markieren; mehrere
Varianten sind möglich; nach dem Laden einer Variante ohne Hausnummern fehlen diese in Karte und Legende.
- [ ] Neue Tests grün, Baseline nicht verschlechtert, Testkatalog aktualisiert, Ablauf in QGIS 3.44 und 4.x geprüft.
## 9. Risiken
- **Blockierender Netzwerkzugriff** im Hauptthread bei großen Gebieten → kachelweise mit Fortschrittsanzeige und
Abbruch; Hinweis bei mehr als z. B. 400 Kacheln.
- **Neue Basemap-Stilversion** (heute v2) → Katalog veraltet; Skript erneut laufen lassen, unbekannte IDs fallen
nach „Sonstiges".
- **Kachelränder:** Features sind an Kachelgrenzen beschnitten und können mehrfach vorkommen – für die Frage
„kommt vor" unerheblich.
- **Umfang:** Stilvorlagen sind fachlich eigenständig. Werden sie zu groß für einen PR, als eigenes Ticket
abtrennen (Schritte 1–6 bleiben #88).