From a841914fcdebe9536c3dc7b1b7c5dff633058f67 Mon Sep 17 00:00:00 2001 From: Marc Froehlich Date: Tue, 30 Jun 2026 01:56:13 +0200 Subject: [PATCH] Fixed Map: leaflet rendering in Java 21 Webview (disabling 3D in leaflet) --- .../view/map/MapHtmlResources.java | 82 +++++++++--- .../kst4contest/view/map/StationMapView.java | 126 ++++++++++++++---- .../kst4contest/view/map/TileProxyServer.java | 9 +- 3 files changed, 166 insertions(+), 51 deletions(-) diff --git a/src/main/java/kst4contest/view/map/MapHtmlResources.java b/src/main/java/kst4contest/view/map/MapHtmlResources.java index 8b78af3..ab6223b 100644 --- a/src/main/java/kst4contest/view/map/MapHtmlResources.java +++ b/src/main/java/kst4contest/view/map/MapHtmlResources.java @@ -214,6 +214,11 @@ public final class MapHtmlResources {
+ @@ -366,18 +371,31 @@ public final class MapHtmlResources { applyThemeClass(); map = L.map('map', { - zoomControl: true + zoomControl: true, + zoomAnimation: false, + fadeAnimation: false, + markerZoomAnimation: false, + inertia: false }).setView([51.0, 10.0], 6); jsLog('Leaflet map initialized'); - L.tileLayer( + const tileLayer = L.tileLayer( 'http://127.0.0.1:' + window._kstTileProxyPort + '/tiles/{s}/{z}/{x}/{y}.png', { maxZoom: 18, + updateWhenZooming: false, + keepBuffer: 4, attribution: '© OpenStreetMap' } - ).addTo(map); + ); + + tileLayer.on('tileerror', function (event) { + const source = event && event.tile ? event.tile.src : 'unknown'; + jsError('OSM tile load failed: ' + source); + }); + + tileLayer.addTo(map); map.createPane('beamPane'); map.getPane('beamPane').style.zIndex = 410; @@ -416,22 +434,47 @@ public final class MapHtmlResources { } function invalidateSize() { - if (!map) { - return; - } - - jsLog('invalidateSize'); - - map.invalidateSize(false); - - if (invalidateNotifyTimer) { - window.clearTimeout(invalidateNotifyTimer); - } - - invalidateNotifyTimer = window.setTimeout(function () { - notifyViewport(); - }, 120); - } + if (!map) { + return; + } + + const mapElement = document.getElementById('map'); + const domWidth = mapElement ? mapElement.clientWidth : -1; + const domHeight = mapElement ? mapElement.clientHeight : -1; + + jsLog('invalidateSize dom=' + domWidth + 'x' + domHeight + + ' leafletBefore=' + map.getSize().x + 'x' + map.getSize().y); + + window.requestAnimationFrame(function () { + map.invalidateSize({ + animate: false, + pan: false, + debounceMoveend: true + }); + + window.setTimeout(function () { + map.invalidateSize({ + animate: false, + pan: false, + debounceMoveend: true + }); + + jsLog('invalidateSize leafletAfter=' + map.getSize().x + 'x' + map.getSize().y); + + if (invalidateNotifyTimer) { + window.clearTimeout(invalidateNotifyTimer); + } + + invalidateNotifyTimer = window.setTimeout(function () { + notifyViewport(); + }, 120); + }, 80); + }); + } + + function resize() { + invalidateSize(); + } function zoomIn() { if (!map) { @@ -693,6 +736,7 @@ public final class MapHtmlResources { return { init: init, invalidateSize: invalidateSize, + resize: resize, zoomIn: zoomIn, zoomOut: zoomOut, inspectPoint: inspectPoint, diff --git a/src/main/java/kst4contest/view/map/StationMapView.java b/src/main/java/kst4contest/view/map/StationMapView.java index 82250a7..066b8a1 100644 --- a/src/main/java/kst4contest/view/map/StationMapView.java +++ b/src/main/java/kst4contest/view/map/StationMapView.java @@ -49,6 +49,17 @@ public final class StationMapView { private final Stage stage = new Stage(); private final WebView webView = new WebView(); private final WebEngine webEngine = webView.getEngine(); + + /** + * Keep a strong Java reference to the bridge object. + * + * JavaFX WebView/JSObject does not guarantee that a Java object passed through + * window.setMember(...) remains strongly reachable from the Java side. Newer + * JavaFX/WebKit/GC combinations can otherwise lose callbacks after a while. + */ + private final JavaMapBridge javaMapBridge = new JavaMapBridge(); + + private TileProxyServer tileProxyServer; private Scene scene; @@ -85,6 +96,22 @@ public final class StationMapView { private List lastSnapshots = List.of(); private MapCallsignRawSnapshot lastSelectedSnapshot; private boolean filteredViewActive; + /** + * Last JSON payloads sent into Leaflet. Used to avoid clearing/recreating + * layers every score/table refresh. Rebuilding DOM markers every few seconds + * is visible as flicker in JavaFX 21 WebView. + */ + private String lastRenderedStationsJson = ""; + private String lastRenderedBeamJson = ""; + private String lastRenderedConnectionJson = ""; + private String lastRenderedGridJson = ""; + + /** + * Prevent periodic refreshes from panning the map back to the selected station. + * Explicit calls to focusCallsignRaw(...) still pan immediately. + */ + private String lastAutoFocusedCallsignRaw = ""; + private double homeLatitudeDeg = Double.NaN; private double homeLongitudeDeg = Double.NaN; @@ -192,8 +219,11 @@ public final class StationMapView { return; } + String normalizedCallsignRaw = callSignRaw.trim().toUpperCase(Locale.ROOT); + lastAutoFocusedCallsignRaw = normalizedCallsignRaw; + executeMapScriptSafely( - "window.kstMapApi.focusCallsignRaw(" + toJsStringLiteral(callSignRaw.trim().toUpperCase(Locale.ROOT)) + ");" + "window.kstMapApi.focusCallsignRaw(" + toJsStringLiteral(normalizedCallsignRaw) + ");" ); } @@ -292,6 +322,8 @@ public final class StationMapView { webView.setMinWidth(0); webView.setMinHeight(220); webView.setPrefHeight(420); + webView.setMaxWidth(Double.MAX_VALUE); + webView.setMaxHeight(Double.MAX_VALUE); VBox.setVgrow(webView, Priority.ALWAYS); profileSection.setMinWidth(0); @@ -623,7 +655,7 @@ public final class StationMapView { webEngine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> { if (newState == javafx.concurrent.Worker.State.SUCCEEDED) { JSObject window = (JSObject) webEngine.executeScript("window"); - window.setMember("javaMapBridge", new JavaMapBridge()); + window.setMember("javaMapBridge", javaMapBridge); executeMapScriptSafely("window.kstMapApi.init();"); @@ -643,11 +675,13 @@ public final class StationMapView { return; } - Platform.runLater(() -> - Platform.runLater(() -> - executeMapScriptSafely("window.kstMapApi.invalidateSize();"))); + Platform.runLater(() -> Platform.runLater(() -> + executeMapScriptSafely("window.kstMapApi.resize();") + )); } + + private void handleWebViewClick(MouseEvent event) { logWebViewMouseEvent("MOUSE_CLICKED", event); @@ -910,9 +944,6 @@ public final class StationMapView { detailPathProfileChart.setObstructionSummary(result.obstructionSummary()); } - - - private void renderAll() { if (!mapReady) { return; @@ -930,49 +961,78 @@ public final class StationMapView { renderBeam(); renderConnectionLine(); renderGridIfViewportKnown(); + focusSelectedStationOnlyWhenSelectionChanged(); + } - if (lastSelectedSnapshot != null) { - focusCallsignRaw(lastSelectedSnapshot.callSignRaw()); + private void focusSelectedStationOnlyWhenSelectionChanged() { + if (lastSelectedSnapshot == null || lastSelectedSnapshot.callSignRaw() == null) { + lastAutoFocusedCallsignRaw = ""; + return; } + + String selectedCallsignRaw = lastSelectedSnapshot.callSignRaw().trim().toUpperCase(Locale.ROOT); + if (selectedCallsignRaw.isBlank() || selectedCallsignRaw.equals(lastAutoFocusedCallsignRaw)) { + return; + } + + focusCallsignRaw(selectedCallsignRaw); } private void renderStations() { + String stationsJson = toStationsJson(lastSnapshots); + if (stationsJson.equals(lastRenderedStationsJson)) { + return; + } + + lastRenderedStationsJson = stationsJson; + executeMapScriptSafely( - "window.kstMapApi.setStations(" + toJsStringLiteral(toStationsJson(lastSnapshots)) + ");" + "window.kstMapApi.setStations(" + toJsStringLiteral(stationsJson) + ");" ); } private void renderBeam() { - if (!Double.isFinite(homeLatitudeDeg) - || !Double.isFinite(homeLongitudeDeg) - || beamWidthDeg <= 0.0 - || maxQrbKm <= 0.0) { + String beamJson = "null"; - executeMapScriptSafely("window.kstMapApi.setBeam('null');"); - return; + if (Double.isFinite(homeLatitudeDeg) + && Double.isFinite(homeLongitudeDeg) + && beamWidthDeg > 0.0 + && maxQrbKm > 0.0) { + + List sectorPoints = buildBeamPolygon(homeLatitudeDeg, homeLongitudeDeg, antennaAzimuthDeg, beamWidthDeg, maxQrbKm); + beamJson = toPointArrayJson(sectorPoints); } - List sectorPoints = buildBeamPolygon(homeLatitudeDeg, homeLongitudeDeg, antennaAzimuthDeg, beamWidthDeg, maxQrbKm); + if (beamJson.equals(lastRenderedBeamJson)) { + return; + } + lastRenderedBeamJson = beamJson; + executeMapScriptSafely( - "window.kstMapApi.setBeam(" + toJsStringLiteral(toPointArrayJson(sectorPoints)) + ");" + "window.kstMapApi.setBeam(" + toJsStringLiteral(beamJson) + ");" ); } private void renderConnectionLine() { - if (lastSelectedSnapshot == null || !lastSelectedSnapshot.hasUsablePosition() - || !Double.isFinite(homeLatitudeDeg) || !Double.isFinite(homeLongitudeDeg)) { + String connectionJson = "null"; - executeMapScriptSafely("window.kstMapApi.setConnection('null');"); - return; + if (lastSelectedSnapshot != null && lastSelectedSnapshot.hasUsablePosition() + && Double.isFinite(homeLatitudeDeg) && Double.isFinite(homeLongitudeDeg)) { + + List points = List.of( + new double[]{homeLatitudeDeg, homeLongitudeDeg}, + new double[]{lastSelectedSnapshot.latitudeDeg(), lastSelectedSnapshot.longitudeDeg()} + ); + connectionJson = toPointArrayJson(points); } - List points = List.of( - new double[]{homeLatitudeDeg, homeLongitudeDeg}, - new double[]{lastSelectedSnapshot.latitudeDeg(), lastSelectedSnapshot.longitudeDeg()} - ); + if (connectionJson.equals(lastRenderedConnectionJson)) { + return; + } + lastRenderedConnectionJson = connectionJson; executeMapScriptSafely( - "window.kstMapApi.setConnection(" + toJsStringLiteral(toPointArrayJson(points)) + ");" + "window.kstMapApi.setConnection(" + toJsStringLiteral(connectionJson) + ");" ); } @@ -1011,11 +1071,19 @@ public final class StationMapView { + " cellPx=" + String.format(Locale.US, "%.1f/%.1f", renderPlan.estimatedCellWidthPx(), renderPlan.estimatedCellHeightPx()) + " cells=" + visibleGridCells.size()); + String gridJson = toGridJson(visibleGridCells, renderPlan); + if (gridJson.equals(lastRenderedGridJson)) { + return; + } + lastRenderedGridJson = gridJson; + executeMapScriptSafely( - "window.kstMapApi.setGrid(" + toJsStringLiteral(toGridJson(visibleGridCells, renderPlan)) + ");" + "window.kstMapApi.setGrid(" + toJsStringLiteral(gridJson) + ");" ); } + + private List buildBeamPolygon(double startLatDeg, double startLonDeg, double centerAzimuthDeg, diff --git a/src/main/java/kst4contest/view/map/TileProxyServer.java b/src/main/java/kst4contest/view/map/TileProxyServer.java index 4d71fbc..8311440 100644 --- a/src/main/java/kst4contest/view/map/TileProxyServer.java +++ b/src/main/java/kst4contest/view/map/TileProxyServer.java @@ -19,8 +19,10 @@ import java.util.concurrent.Executors; */ final class TileProxyServer { - private static final int CACHE_MAX = 512; + private static final int CACHE_MAX = 2048; private static final String USER_AGENT = "kst4contest/1.0 amateur-radio-contest-tool"; + private static final int TILE_PROXY_THREADS = 12; + private static final int TILE_PROXY_BACKLOG = 128; private final ServerSocket serverSocket; private final ExecutorService executor; @@ -39,9 +41,9 @@ final class TileProxyServer { } }); - this.serverSocket = new ServerSocket(0, 16, InetAddress.getByName("127.0.0.1")); + this.serverSocket = new ServerSocket(0, TILE_PROXY_BACKLOG, InetAddress.getByName("127.0.0.1")); - this.executor = Executors.newFixedThreadPool(4, r -> { + this.executor = Executors.newFixedThreadPool(TILE_PROXY_THREADS, r -> { Thread t = new Thread(r, "tile-proxy"); t.setDaemon(true); return t; @@ -132,6 +134,7 @@ final class TileProxyServer { + "Content-Type: image/png\r\n" + "Content-Length: " + tileData.length + "\r\n" + "Cache-Control: max-age=86400\r\n" + + "Access-Control-Allow-Origin: *\r\n" + "Connection: close\r\n" + "\r\n"; out.write(header.getBytes());