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