Files
kst4contest/docs/superpowers/plans/2026-10-01-etappe6-karte-compose-parity.md
T
Rsclub2_2 cc8c90204e Draw the whole interface in Compose
Etappen 3 to 6a of the migration (129 commits). Every window the operator sees
is Compose; the JavaFX ones still exist beside them so the two could be
compared against a live session, which is the only way the differences came to
light.

The toolchain first, then the settings window with its eleven tabs and 72 real
preferences; then the multi-column table as a component, and with it the rules
it needs -- stable row identity, a selection that survives a refresh, column
order the operator can drag, and a repaint when the rows change rather than
when the list instance does. Then the update and monitor windows, and the main
window: 22 station columns, the filter bar, the selected-station panel, the
message tabs, the priority bar, the sked controls, the status indicators and
the timeline, assembled behind a splitter because Compose has none.

The map stops being a WebView. A Compose canvas projects WebMercator tiles
itself, with the clustering in core and an asynchronous fetcher in place of the
local tile proxy; derive() becomes a pure Kotlin reimplementation instead of a
reflection call into com.sun.javafx.

Most of the volume here is parity the operator would notice rather than new
behaviour: desktop density instead of Material's phone minimums, colours taken
from the JavaFX stylesheets instead of Material's palette, row dividers, band
columns, grid-square labels and their colours, the home marker and antenna
beam, pinch-to-zoom under Wayland, and layouts that survive a window narrower
than the chart they stack.

Two lessons are worth more than the diff. A control bound to a value Compose
cannot observe writes through and never redraws -- that one shipped repeatedly
with a green build, because no test drew anything. And a layout that clips
instead of wrapping is worse than one that is merely too narrow: half a word
is the worst available outcome.
2026-10-05 22:33:28 +02:00

201 lines
10 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.
# Umsetzungsplan Etappe 6 — Karte: Angleichung an die Leaflet-Fassung
Spezifikation: `docs/superpowers/specs/2026-09-25-compose-migration-design.md`, Etappe 6.
## Was dieser Plan ist — und was nicht
Etappe 6 hat zwei Hälften: die Karte nach Compose bringen, und JavaFX aus dem Build
werfen. Die erste Hälfte wurde bereits angefangen (im Übergabeprotokoll „Task 18/19"),
und das Ergebnis trägt nicht. Dieser Plan ist die **Nachbesserung dieser ersten
Hälfte**. Das Entfernen von JavaFX und der dafür nötige Ersatz von
`JavaFxStylesheet.derive` bleiben ausdrücklich außen vor.
Der Anlass ist nicht Geschmack. Die Compose-Karte bildet mehrere Mechanismen der
Leaflet-Fassung **gar nicht** ab, nicht bloß anders. Drei davon liegen fertig und
getestet in `core` und werden nicht aufgerufen.
## Beweislage
Verglichen wurde `github_docs/station_map_compact.png` und
`github_docs/station_map_path_analysis.png` (JavaFX) gegen den Stand vom 30.09.
(`scratch/screenshots/AAA011.png`, `AAA012.png`).
| # | Befund | Stelle |
|---|---|---|
| 1 | `MaidenheadGridRenderPlanner` wird nicht aufgerufen — jede Zelle **und jedes Label** wird gezeichnet | `ComposeStationMap.kt:286` gegen `StationMapView.java:1301` |
| 2 | Gitterlabel in der Ecke statt in der Zellenmitte, weiß statt `#63067a` | `ComposeStationMap.kt:307` gegen `MapHtmlResources.java:302` |
| 3 | Gebrochener Zoom mit `scale()` über die ganze Zeichenfläche; alle Größen werden mit `/ scaleFactor` gegengerechnet | `ComposeStationMap.kt:243` |
| 4 | Clustering: 50 px fest, ab 2 Stationen, immer an — statt 55/70/95/125 px je Zoom, ab 3 Stationen, aus ab Zoom 8 | `ComposeStationMap.kt:68` gegen `MapHtmlResources.java:393-410`, `:758` |
| 5 | Kopfzeile: nur „Selected: X" statt der sechs Felder; rohe Material-3-Knöpfe statt `Fields.button` | `StationMapWindow.kt:159` gegen `StationMapView.java:571`, `:1010` |
| 6 | Zoomknöpfe und OSM-Attribution fehlen | — |
| 7 | Kein Kartenhintergrund; fehlende Kacheln zeigen Systemgrau | gegen `MapHtmlResources.java:57`, `:70` |
| 8 | `TileFetcher`: abgekündigte a/b/c-Subdomains, nur RAM-Cache, Fehler werden still verschluckt | `TileFetcher.java:44` |
| 9 | Absturz „maxWidth must be >= than minWidth", übertüncht durch eine Sperre, die das Fenster unter 500×400 dp leer lässt | `StationMapWindow.kt:151`, `:329` |
**Der dritte Befund ist die Wurzel von mehreren anderen.** Weil die gesamte
Zeichenfläche skaliert wird, hängen Punktradius, Strichstärke und Schriftgröße am
Zoom-Bruchteil. In der Leaflet-Fassung war ein Marker **immer** 12 px und ein Label
**immer** 12 px, weil Leaflet Overlays in Bildschirmkoordinaten zeichnet. Solange der
`scale()`-Block steht, ist jede Farb- und Größenkorrektur nur an einer Zoomstufe
richtig — und genau das erklärt, warum mehrere Anläufe nacheinander „fast richtig"
aussahen.
## Getroffene Entscheidungen
| Frage | Entscheidung |
|---|---|
| Zielbild | Nah genug an Leaflet, eigene Handschrift in Details erlaubt — gleiche Informationsdichte und Bedienlogik sind Pflicht |
| Zoom | Weich zoomen, aber **Overlay in Bildschirmkoordinaten**. Nicht der ganzzahlige Zoom der JavaFX-Fassung |
| Absturz | Zuerst reproduzieren und beheben, dann umbauen |
| Kacheln | `TileFetcher` reparieren und um einen Plattencache erweitern |
**Abweichung von der Spezifikation, bewusst.** Die Spezifikation schreibt „Kacheln
über den bestehenden `TileProxyServer`". Der Proxy existiert, weil die JavaFX-WebView
in AppImage- und Flatpak-Verpackung an TLS scheiterte — ein Compose-Canvas holt die
Kacheln selbst und hat dieses Problem nicht. Der Proxy bleibt für den JavaFX-Pfad
stehen, bis dieser entfällt.
**Der ganzzahlige Zoom war ebenfalls ein WebView-Zugeständnis**, nicht Absicht:
`MapHtmlResources.java:884` begründet `zoomSnap: 1` mit unzuverlässiger
Markerpositionierung in der JavaFX-WebView. Der Grund entfällt mit der WebView.
## Reihenfolge
Erst der Absturz, dann die Zeichenebene, dann die Fachregeln, zuletzt die Hülle.
Umgekehrt müsste jeder Schritt gegen einen Fehler arbeiten, der ihn jederzeit
abbrechen kann.
## Aufgaben
### 1. Absturz beheben, Sperre entfernen
`StationMapWindow.kt:328-330` legt in **eine** `Row` ein `Text` mit fester
`Modifier.width(130.dp)` und daneben eine `Box(Modifier.weight(1f))`. Wird das rechte
Pane schmaler als diese 130 dp samt Innenabstand, bleibt für das gewichtete Kind eine
**negative** Restbreite — und daraus baut Compose die `Constraints`, die mit
„maxWidth must be >= than minWidth, maxHeight must be >= than minHeight, minWidth and
minHeight must be >= 0" abbrechen.
Dazu fehlt dem Detailbereich die Mindestbreite, die JavaFX hatte:
`detailScrollPane.setMinWidth(210)` und `setPrefWidth(350)`, `StationMapView.java:468`.
- Reproduzieren: Teiler ganz nach rechts ziehen.
- Feste Breite durch `weight` ersetzen, wie es `DetailRow` bereits macht.
- Mindestbreite des Detailbereichs im `SplitterState` verankern, nicht im Fenster.
- **Erst danach** die Sperre `BoxWithConstraints { if (maxWidth < 500.dp …) return@… }`
in Zeile 151 entfernen.
**Abnahme:** Der Teiler lässt sich über die gesamte Breite ziehen, ohne dass ein
Dialog erscheint, und das Fenster bleibt bei jeder Größe gezeichnet.
### 2. Zeichenebene: Welt- gegen Bildschirmkoordinaten trennen
Eine Projektionshilfe (Zentrum, Zoom als `Float`, Canvasgröße) rechnet den gebrochenen
Zoom über `2^zoom` direkt ein, statt mit `floor(zoom)` zu rechnen und die Zeichenfläche
hinterher zu skalieren.
- Nur die Kachelschicht wird skaliert gezeichnet: Kacheln der Stufe `floor(zoom)`,
Zielgröße über den Skalierungsfaktor.
- Gitter, Marker, Labels, Antennenkeule, Pfadlinie und Kurzhinweis zeichnen
**unskaliert in Bildschirmpixeln**.
- `scale()` und sämtliche `/ scaleFactor` entfallen.
- Kartenhintergrund `#ede9df` hell, `#23282d` dunkel als erste Zeichenoperation.
- Zoombereich 3–18 wie Leaflet; Compose begrenzt heute auf 4–18.
**Abnahme:** Ein Marker behält seine Größe über den gesamten Zoombereich. Bei
abgeschalteten Kacheln ist die Fläche in der Kartenfarbe, nicht in Systemgrau.
### 3. Gitter über den vorhandenen Planer
`MaidenheadGridRenderPlanner.createPlan(floor(zoom), bbox, canvasW, canvasH)` aufrufen,
Zellen über `buildVisibleCells(…, plan.precision())` holen.
- Label nur bei `plan.shouldShowLabel(cell)`, Schriftgröße aus `plan.labelFontSizePx()`.
- Label **mittig** in der Zelle, in `#63067a` auf halbdurchsichtigem Kästchen.
- Linien `#e1e7ec` bei 48 % dunkel, `#46586c` bei 56 % hell, Strichstärke 1.4.
Der Planer ist vorhanden und wird nicht geändert.
**Abnahme:** Bei weitem Zoom Zweistellen-Felder, bei mittlerem die vierstelligen
Quadrate wie in `station_map_compact.png`; keine Labelteppiche.
### 4. Clustering nach den Leaflet-Regeln
`StationMapClusterer` in `core` vom Nächster-Nachbar-Verfahren auf das Bildschirmraster
der Leaflet-Fassung umstellen:
- Zellgröße 55 px ab Zoom 7, 70 px ab 6, 95 px ab 5, darunter 125 px.
- Bucket über `floor(bildschirmX / zellgröße)`.
- Cluster **erst ab 3** Stationen; darunter werden die Stationen einzeln gezeichnet.
- Clustering **aus ab Zoom ≥ 8**.
- Ausgewählte Stationen und solche mit `warningToMyDirection` werden **nie** geclustert.
- Blasendurchmesser 32 / 36 / 42 px ab 1 / 8 / 20 Stationen.
Das sind Fachregeln, keine Zeichenregeln — sie gehören mit Tests nach `core`. Die
Prüfung der Konstanten gegen `MapHtmlResources.java:393-410` und `:758` ist Teil der
Aufgabe.
**Abnahme:** Tests in `core` decken die vier Zellgrößen, die Schwelle von drei
Stationen, die Abschaltung ab Zoom 8 und beide Ausnahmen ab.
### 5. Kopfzeile und Bedienelemente
- Statuszeile mit dem vollen Text aus `updateStatusLabel()`
(`StationMapView.java:1010`): `Showing N visible stations`, optional
`| filtered view active`, `| Selected: …`, Locator, `km / °`, Bänder, QRG.
- Statuszeile mit `Modifier.weight(1f)`, damit die Knöpfe rechtsbündig stehen.
- `Fields.button` (`FormControls.kt:367`) und `CompactCheckbox` (`:556`) statt roher
Material-3-Bedienelemente. Die grünen Pillen entstehen, weil `Theme.kt:97`
`primary` auf den Grünton des Abend-Stylesheets legt und Material `Button` genau
diese Rolle als Flächenfarbe nimmt.
- Zoomknöpfe oben links als Überlagerung.
- OSM-Attribution unten rechts. Die ist bei OSM-Kacheln lizenzrechtlich nicht
freigestellt.
**Abnahme:** Die Kopfzeile zeigt dieselben Angaben wie `station_map_compact.png` und
sieht aus wie die übrigen Compose-Fenster.
### 6. Kacheln
- Host `tile.openstreetmap.org`; die Subdomains `a`/`b`/`c` sind abgekündigt.
- Plattencache im Profilverzeichnis, nicht im Arbeitsverzeichnis.
- Fehler nach außen melden, damit die Karte „Kacheln nicht erreichbar" anzeigen kann,
statt wie in `AAA012.png` kommentarlos grau zu bleiben.
- `User-Agent` bleibt wie bisher; die OSM-Nutzungsbedingungen verlangen einen
sprechenden Wert.
**Abnahme:** Nach einem Neustart ohne Netz zeigt die Karte die zuletzt geholten
Kacheln aus dem Plattencache und einen Hinweis für die fehlenden.
## Was unverändert bleibt
- Der **gesamte JavaFX-Pfad**: `StationMapView.java`, `MapHtmlResources.java`,
`StationMapBridge.java`, `TileProxyServer.java`. Sie sind bis zum Entfernen von
JavaFX die Referenz.
- Die Fachlogik in `core` wird **benutzt, nicht geändert**: `PathAnalysis*`,
`Terrain*`, `PathGeometryUtils`, `MapCallsignRawSnapshotBuilder`,
`MaidenheadGridUtils`, `MaidenheadGridRenderPlanner`.
- `GUIstationMapClusteringEnabled` bleibt eine Layout-Einstellung unterhalb
`guiOptions` mit Vorgabewert `true`.
- Zurücksetzen der Ansicht lässt den Zoom in Ruhe und löscht nur das Ziel (AGENTS.md).
- Die `/cq`-Vorbelegung bei Stationsauswahl.
- Der Zoom auf die Bounding Box zwischen eigenem Standort und Zielstation.
## Abnahmekriterium der Etappe
Die Karte zeigt dieselben Informationen wie die Leaflet-Fassung. Offline-DEM,
Terrain-Pakete und der Profil-Cache funktionieren unverändert.
Das zweite Kriterium der Spezifikation — keine JavaFX-Abhängigkeit mehr im Projekt —
gehört zur zweiten Hälfte der Etappe und ist hier **nicht** erfüllt.
## Offene Punkte
- Leistungsverhalten des Canvas bei mehreren tausend Markern. Die Spezifikation nennt
als Ausweg eine Vorabaggregation im Kern; Schritt 4 ist genau diese Vorstufe und
sollte vor einer Messung fertig sein.
- `JavaFxStylesheet.derive` ruft `com.sun.javafx.util.Utils.deriveColor` über
Reflexion auf. Das muss **vor** dem Entfernen von JavaFX ersetzt werden, sonst
verschieben sich alle abgeleiteten Farben still. Nicht Teil dieses Plans, aber Teil
dieser Etappe.