Added a map to show where other stn are // refactored message adding to tables for performance, max 30.000 msg now

This commit is contained in:
Marc Froehlich
2026-06-22 20:15:34 +02:00
committed by Philipp Wagner
parent 1ce468145c
commit e898f6875d
7 changed files with 144 additions and 1142 deletions
+2
View File
@@ -0,0 +1,2 @@
S53MM
PA9R
File diff suppressed because it is too large Load Diff
@@ -1,10 +1,5 @@
package kst4contest.view.map; package kst4contest.view.map;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
/** /**
* HTML host for the JavaFX WebView map. * HTML host for the JavaFX WebView map.
* *
@@ -21,21 +16,7 @@ public final class MapHtmlResources {
private MapHtmlResources() { private MapHtmlResources() {
} }
private static String readRequiredResource(String resourcePath) { public static String createStationMapHtml() {
try (InputStream inputStream = MapHtmlResources.class.getResourceAsStream(resourcePath)) {
if (inputStream == null) {
throw new IllegalStateException("Missing map resource: " + resourcePath);
}
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException exception) {
throw new UncheckedIOException("Could not read map resource: " + resourcePath, exception);
}
}
public static String createStationMapHtml(int tileProxyPort) {
String leafletCss = readRequiredResource("/web/leaflet/leaflet.css");
String leafletJs = readRequiredResource("/web/leaflet/leaflet.js");
return """ return """
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@@ -43,9 +24,10 @@ public final class MapHtmlResources {
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KST4Contest Station Map</title> <title>KST4Contest Station Map</title>
<style> <link rel="stylesheet"
""" + leafletCss + """ href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
</style> integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="">
<style> <style>
:root { :root {
--map-background: #ede9df; --map-background: #ede9df;
@@ -214,10 +196,9 @@ public final class MapHtmlResources {
<body class="kst-theme-light"> <body class="kst-theme-light">
<div id="map"></div> <div id="map"></div>
<script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
""" + leafletJs + """ integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
</script> crossorigin=""></script>
<script>window._kstTileProxyPort=__TILE_PROXY_PORT__;</script>
<script> <script>
@@ -355,12 +336,7 @@ public final class MapHtmlResources {
function init() { function init() {
if (map) { if (map) {
return true; return;
}
if (typeof L === 'undefined') {
jsError('Leaflet is not loaded. Station map cannot initialize.');
return false;
} }
applyThemeClass(); applyThemeClass();
@@ -371,13 +347,10 @@ public final class MapHtmlResources {
jsLog('Leaflet map initialized'); jsLog('Leaflet map initialized');
L.tileLayer( L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
'http://127.0.0.1:' + window._kstTileProxyPort + '/tiles/{s}/{z}/{x}/{y}.png', maxZoom: 18,
{ attribution: '&copy; OpenStreetMap'
maxZoom: 18, }).addTo(map);
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}
).addTo(map);
map.createPane('beamPane'); map.createPane('beamPane');
map.getPane('beamPane').style.zIndex = 410; map.getPane('beamPane').style.zIndex = 410;
@@ -412,7 +385,6 @@ public final class MapHtmlResources {
notifyMapReady(); notifyMapReady();
notifyViewport(); notifyViewport();
return true;
} }
function invalidateSize() { function invalidateSize() {
@@ -496,17 +468,13 @@ public final class MapHtmlResources {
} }
function setHome(lat, lon, zoom) { function setHome(lat, lon, zoom) {
if (!init()) { init();
return;
}
jsLog('setHome lat=' + lat + ' lon=' + lon + ' zoom=' + zoom); jsLog('setHome lat=' + lat + ' lon=' + lon + ' zoom=' + zoom);
map.setView([lat, lon], zoom); map.setView([lat, lon], zoom);
} }
function setStations(stationsJson) { function setStations(stationsJson) {
if (!init()) { init();
return;
}
stationLayer.clearLayers(); stationLayer.clearLayers();
markersByCallsignRaw = {}; markersByCallsignRaw = {};
@@ -535,9 +503,7 @@ public final class MapHtmlResources {
} }
function setBeam(beamJson) { function setBeam(beamJson) {
if (!init()) { init();
return;
}
beamLayer.clearLayers(); beamLayer.clearLayers();
if (!beamJson || beamJson === 'null') { if (!beamJson || beamJson === 'null') {
@@ -564,9 +530,7 @@ public final class MapHtmlResources {
} }
function setConnection(connectionJson) { function setConnection(connectionJson) {
if (!init()) { init();
return;
}
connectionLayer.clearLayers(); connectionLayer.clearLayers();
if (!connectionJson || connectionJson === 'null') { if (!connectionJson || connectionJson === 'null') {
@@ -591,9 +555,7 @@ public final class MapHtmlResources {
} }
function setProfileHoverPoint(point) { function setProfileHoverPoint(point) {
if (!init()) { init();
return;
}
if (profileHoverMarker) { if (profileHoverMarker) {
map.removeLayer(profileHoverMarker); map.removeLayer(profileHoverMarker);
@@ -622,9 +584,7 @@ public final class MapHtmlResources {
} }
function setGrid(gridJson) { function setGrid(gridJson) {
if (!init()) { init();
return;
}
gridLayer.clearLayers(); gridLayer.clearLayers();
const cells = JSON.parse(gridJson); const cells = JSON.parse(gridJson);
@@ -710,6 +670,6 @@ public final class MapHtmlResources {
</script> </script>
</body> </body>
</html> </html>
""".replace("__TILE_PROXY_PORT__", String.valueOf(tileProxyPort)); """;
} }
} }
@@ -6,6 +6,7 @@ import javafx.collections.ListChangeListener;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import javafx.util.Duration; import javafx.util.Duration;
import kst4contest.controller.ChatController; import kst4contest.controller.ChatController;
import kst4contest.locatorUtils.Location;
import kst4contest.model.ChatMember; import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences; import kst4contest.model.ChatPreferences;
@@ -14,13 +15,12 @@ import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer; import java.util.function.Consumer;
import kst4contest.model.Band;
import java.util.function.Predicate;
/** /**
* Synchronizes the application state with the station map window. * Synchronizes the application state with the station map window.
* *
@@ -32,7 +32,7 @@ import java.util.function.Predicate;
*/ */
public final class StationMapBridge { public final class StationMapBridge {
private final ExecutorService pathAnalysisExecutor = Executors.newSingleThreadExecutor(new PathAnalysisThreadFactory());
private final AtomicLong pathAnalysisGeneration = new AtomicLong(0); private final AtomicLong pathAnalysisGeneration = new AtomicLong(0);
private final ChatController chatController; private final ChatController chatController;
@@ -45,7 +45,8 @@ public final class StationMapBridge {
private final MapCallsignRawSnapshotBuilder snapshotBuilder = new MapCallsignRawSnapshotBuilder(); private final MapCallsignRawSnapshotBuilder snapshotBuilder = new MapCallsignRawSnapshotBuilder();
// private final OfflineDemManager offlineDemManager = new OfflineDemManager();
private final PathAnalysisService pathAnalysisService;
private String lastPathAnalysisRequestSignature = ""; private String lastPathAnalysisRequestSignature = "";
@@ -63,7 +64,9 @@ public final class StationMapBridge {
this.refreshCoalescer.setOnFinished(event -> refreshNow()); this.refreshCoalescer.setOnFinished(event -> refreshNow());
this.pathAnalysisService = new GeometryOnlyPathAnalysisService(
new OpenMeteoTerrainProfileProvider()
);
} }
public void install() { public void install() {
@@ -82,10 +85,6 @@ public final class StationMapBridge {
(obs, oldValue, newValue) -> scheduleRefresh() (obs, oldValue, newValue) -> scheduleRefresh()
); );
chatController.getLst_chatMemberListFilterPredicates().addListener(
(ListChangeListener<Predicate<ChatMember>>) change -> requestImmediateRefresh()
);
requestImmediateRefresh(); requestImmediateRefresh();
} }
@@ -171,17 +170,6 @@ public final class StationMapBridge {
requestPathAnalysisAsync(preferences.getStn_loginLocatorMainCat(), selectedSnapshot); requestPathAnalysisAsync(preferences.getStn_loginLocatorMainCat(), selectedSnapshot);
} }
/**
* Requests the selected station path analysis through the central reachability
* service.
*
* <p>The map no longer owns a separate PathAnalysisService. This ensures that
* the map detail panel, the Tropo table column and the station filters all use
* exactly the same path/link-budget calculation.</p>
*
* @param ownLocator6 own locator from preferences
* @param selectedSnapshot selected map marker snapshot
*/
private void requestPathAnalysisAsync(String ownLocator6, MapCallsignRawSnapshot selectedSnapshot) { private void requestPathAnalysisAsync(String ownLocator6, MapCallsignRawSnapshot selectedSnapshot) {
String normalizedOwnLocator6 = normalizeLocator6(ownLocator6); String normalizedOwnLocator6 = normalizeLocator6(ownLocator6);
@@ -214,28 +202,20 @@ public final class StationMapBridge {
PathAnalysisResult.loading(normalizedOwnLocator6, normalizedTargetLocator6, targetCallsignRaw) PathAnalysisResult.loading(normalizedOwnLocator6, normalizedTargetLocator6, targetCallsignRaw)
); );
ChatMember selectedMember = resolveBestChatMember(targetCallsignRaw); pathAnalysisExecutor.submit(() -> {
PathAnalysisResult result = buildPathAnalysisResult(normalizedOwnLocator6, selectedSnapshot);
chatController.getReachabilityService().requestPathAnalysisForMap( Platform.runLater(() -> {
selectedMember, if (generation != pathAnalysisGeneration.get()) {
selectedSnapshot, return;
result -> {
if (generation != pathAnalysisGeneration.get()) {
return;
}
stationMapView.setPathAnalysisResult(result);
} }
); stationMapView.setPathAnalysisResult(result);
});
});
} }
/**
* Invalidates pending map callbacks.
*
* <p>The actual calculation executor is owned by ReachabilityService now, so
* this bridge no longer shuts down any path-analysis thread directly.</p>
*/
public void dispose() { public void dispose() {
pathAnalysisGeneration.incrementAndGet(); pathAnalysisExecutor.shutdownNow();
} }
private void handleMapCallsignSelection(String callSignRaw) { private void handleMapCallsignSelection(String callSignRaw) {
@@ -306,6 +286,43 @@ public final class StationMapBridge {
return callSignRaw.trim().toUpperCase(Locale.ROOT); return callSignRaw.trim().toUpperCase(Locale.ROOT);
} }
private PathAnalysisResult buildPathAnalysisResult(String ownLocator6, MapCallsignRawSnapshot selectedSnapshot) {
String normalizedOwnLocator6 = normalizeLocator6(ownLocator6);
if (selectedSnapshot == null) {
return PathAnalysisResult.waitingForSelection(normalizedOwnLocator6);
}
String normalizedTargetLocator6 = normalizeLocator6(selectedSnapshot.locator6());
if (normalizedOwnLocator6.length() != 6) {
return PathAnalysisResult.waitingForValidHomeLocator(normalizedOwnLocator6, normalizedTargetLocator6);
}
if (!selectedSnapshot.hasUsablePosition()) {
return PathAnalysisResult.waitingForValidTarget(normalizedOwnLocator6, normalizedTargetLocator6);
}
Location homeLocation = new Location(normalizedOwnLocator6);
double analysisFrequencyMHz = resolveAnalysisFrequencyMHz(selectedSnapshot);
PathAnalysisRequest request = new PathAnalysisRequest(
normalizedOwnLocator6,
homeLocation.getLatitude().toDegrees(),
homeLocation.getLongitude().toDegrees(),
selectedSnapshot.callSignRaw(),
normalizedTargetLocator6,
selectedSnapshot.latitudeDeg(),
selectedSnapshot.longitudeDeg(),
analysisFrequencyMHz,
chatController.getChatPreferences().getStn_pathAnalysisOwnAntennaHeightMeters(),
chatController.getChatPreferences().getStn_pathAnalysisDefaultTargetAntennaHeightMeters(),
PathGeometryUtils.DEFAULT_EFFECTIVE_EARTH_RADIUS_FACTOR,
chatController.getChatPreferences().buildPathLinkBudgetSettings()
);
return pathAnalysisService.analyze(request);
}
private double resolveAnalysisFrequencyMHz(MapCallsignRawSnapshot selectedSnapshot) { private double resolveAnalysisFrequencyMHz(MapCallsignRawSnapshot selectedSnapshot) {
if (selectedSnapshot == null) { if (selectedSnapshot == null) {
@@ -359,5 +376,12 @@ public final class StationMapBridge {
return locator.trim().toUpperCase(Locale.ROOT); return locator.trim().toUpperCase(Locale.ROOT);
} }
private static final class PathAnalysisThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable, "station-map-path-analysis");
thread.setDaemon(true);
return thread;
}
}
} }
@@ -18,7 +18,6 @@ import kst4contest.ApplicationConstants;
import kst4contest.locatorUtils.Location; import kst4contest.locatorUtils.Location;
import kst4contest.model.ChatPreferences; import kst4contest.model.ChatPreferences;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@@ -49,7 +48,6 @@ public final class StationMapView {
private final Stage stage = new Stage(); private final Stage stage = new Stage();
private final WebView webView = new WebView(); private final WebView webView = new WebView();
private final WebEngine webEngine = webView.getEngine(); private final WebEngine webEngine = webView.getEngine();
private TileProxyServer tileProxyServer;
private Scene scene; private Scene scene;
private BorderPane rootPane; private BorderPane rootPane;
@@ -129,11 +127,6 @@ public final class StationMapView {
public StationMapView(ChatPreferences chatPreferences) { public StationMapView(ChatPreferences chatPreferences) {
this.chatPreferences = Objects.requireNonNull(chatPreferences, "chatPreferences"); this.chatPreferences = Objects.requireNonNull(chatPreferences, "chatPreferences");
try {
tileProxyServer = new TileProxyServer();
} catch (IOException e) {
System.err.println("[StationMap] tile proxy failed to start: " + e.getMessage());
}
initializeUi(); initializeUi();
initializeWebView(); initializeWebView();
} }
@@ -634,8 +627,7 @@ public final class StationMapView {
} }
}); });
int proxyPort = tileProxyServer != null ? tileProxyServer.getPort() : 0; webEngine.loadContent(MapHtmlResources.createStationMapHtml());
webEngine.loadContent(MapHtmlResources.createStationMapHtml(proxyPort));
} }
private void requestMapInvalidateSize() { private void requestMapInvalidateSize() {
@@ -1101,7 +1093,6 @@ public final class StationMapView {
webEngine.executeScript(script); webEngine.executeScript(script);
} catch (Exception exception) { } catch (Exception exception) {
System.err.println("[StationMap] executeScript failed: " + exception.getMessage()); System.err.println("[StationMap] executeScript failed: " + exception.getMessage());
exception.printStackTrace();
} }
} }
@@ -1452,4 +1443,4 @@ public final class StationMapView {
return storedSize[1]; return storedSize[1];
} }
} }
+1 -1
View File
@@ -18,4 +18,4 @@ module praktiKST {
opens kst4contest.view.map to javafx.web; opens kst4contest.view.map to javafx.web;
} }
+1 -6
View File
@@ -2365,9 +2365,4 @@ OZ1DLD/P;Bent;JO45SK;StringProperty [value: 144.285]; wkd true; wkd144 true; wkd
OZ1FDH;Claus;JO55QX;StringProperty [value: null]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz OZ1FDH;Claus;JO55QX;StringProperty [value: null]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
OZ7KJ;Skive Club;JO46ML;StringProperty [value: 144.225]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz OZ7KJ;Skive Club;JO46ML;StringProperty [value: 144.225]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
SM7VUK;Bengt;JO66LI;StringProperty [value: 144.304]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz SM7VUK;Bengt;JO66LI;StringProperty [value: 144.304]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
OZ1HDF;Ken;JO55UN;StringProperty [value: null]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz OZ1HDF;Ken;JO55UN;StringProperty [value: null]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
DO5SA;unknown;unknown;StringProperty [value: null]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; null
9A2HM;unknown;unknown;StringProperty [value: null]; wkd true; wkd144 false; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; null
9A2HM;null;null;StringProperty [value: null]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; null
9A2HM;null;null;StringProperty [value: null]; wkd true; wkd144 false; wkd432false; wkd1240false; wkd2300true; wkd3400false; wkd5600false; wkd10Gfalse ; null
OV3T;Thomas;JO46CM;StringProperty [value: null]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz