mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-20 06:12:57 +02:00
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.
This commit is contained in:
173 files changed
+26500
-4305
No files matched your search
@@ -0,0 +1,200 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user