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;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
/**
* HTML host for the JavaFX WebView map.
*
@@ -21,21 +16,7 @@ public final class MapHtmlResources {
private MapHtmlResources() {
}
private static String readRequiredResource(String resourcePath) {
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");
public static String createStationMapHtml() {
return """
<!DOCTYPE html>
<html lang="en">
@@ -43,9 +24,10 @@ public final class MapHtmlResources {
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KST4Contest Station Map</title>
<style>
""" + leafletCss + """
</style>
<link rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="">
<style>
:root {
--map-background: #ede9df;
@@ -214,10 +196,9 @@ public final class MapHtmlResources {
<body class="kst-theme-light">
<div id="map"></div>
<script>
""" + leafletJs + """
</script>
<script>window._kstTileProxyPort=__TILE_PROXY_PORT__;</script>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<script>
@@ -355,12 +336,7 @@ public final class MapHtmlResources {
function init() {
if (map) {
return true;
}
if (typeof L === 'undefined') {
jsError('Leaflet is not loaded. Station map cannot initialize.');
return false;
return;
}
applyThemeClass();
@@ -371,13 +347,10 @@ public final class MapHtmlResources {
jsLog('Leaflet map initialized');
L.tileLayer(
'http://127.0.0.1:' + window._kstTileProxyPort + '/tiles/{s}/{z}/{x}/{y}.png',
{
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}
).addTo(map);
attribution: '&copy; OpenStreetMap'
}).addTo(map);
map.createPane('beamPane');
map.getPane('beamPane').style.zIndex = 410;
@@ -412,7 +385,6 @@ public final class MapHtmlResources {
notifyMapReady();
notifyViewport();
return true;
}
function invalidateSize() {
@@ -496,17 +468,13 @@ public final class MapHtmlResources {
}
function setHome(lat, lon, zoom) {
if (!init()) {
return;
}
init();
jsLog('setHome lat=' + lat + ' lon=' + lon + ' zoom=' + zoom);
map.setView([lat, lon], zoom);
}
function setStations(stationsJson) {
if (!init()) {
return;
}
init();
stationLayer.clearLayers();
markersByCallsignRaw = {};
@@ -535,9 +503,7 @@ public final class MapHtmlResources {
}
function setBeam(beamJson) {
if (!init()) {
return;
}
init();
beamLayer.clearLayers();
if (!beamJson || beamJson === 'null') {
@@ -564,9 +530,7 @@ public final class MapHtmlResources {
}
function setConnection(connectionJson) {
if (!init()) {
return;
}
init();
connectionLayer.clearLayers();
if (!connectionJson || connectionJson === 'null') {
@@ -591,9 +555,7 @@ public final class MapHtmlResources {
}
function setProfileHoverPoint(point) {
if (!init()) {
return;
}
init();
if (profileHoverMarker) {
map.removeLayer(profileHoverMarker);
@@ -622,9 +584,7 @@ public final class MapHtmlResources {
}
function setGrid(gridJson) {
if (!init()) {
return;
}
init();
gridLayer.clearLayers();
const cells = JSON.parse(gridJson);
@@ -710,6 +670,6 @@ public final class MapHtmlResources {
</script>
</body>
</html>
""".replace("__TILE_PROXY_PORT__", String.valueOf(tileProxyPort));
""";
}
}
@@ -6,6 +6,7 @@ import javafx.collections.ListChangeListener;
import javafx.scene.control.TableView;
import javafx.util.Duration;
import kst4contest.controller.ChatController;
import kst4contest.locatorUtils.Location;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
@@ -14,13 +15,12 @@ import java.util.Comparator;
import java.util.List;
import java.util.Locale;
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.function.Consumer;
import kst4contest.model.Band;
import java.util.function.Predicate;
/**
* Synchronizes the application state with the station map window.
*
@@ -32,7 +32,7 @@ import java.util.function.Predicate;
*/
public final class StationMapBridge {
private final ExecutorService pathAnalysisExecutor = Executors.newSingleThreadExecutor(new PathAnalysisThreadFactory());
private final AtomicLong pathAnalysisGeneration = new AtomicLong(0);
private final ChatController chatController;
@@ -45,7 +45,8 @@ public final class StationMapBridge {
private final MapCallsignRawSnapshotBuilder snapshotBuilder = new MapCallsignRawSnapshotBuilder();
// private final OfflineDemManager offlineDemManager = new OfflineDemManager();
private final PathAnalysisService pathAnalysisService;
private String lastPathAnalysisRequestSignature = "";
@@ -63,7 +64,9 @@ public final class StationMapBridge {
this.refreshCoalescer.setOnFinished(event -> refreshNow());
this.pathAnalysisService = new GeometryOnlyPathAnalysisService(
new OpenMeteoTerrainProfileProvider()
);
}
public void install() {
@@ -82,10 +85,6 @@ public final class StationMapBridge {
(obs, oldValue, newValue) -> scheduleRefresh()
);
chatController.getLst_chatMemberListFilterPredicates().addListener(
(ListChangeListener<Predicate<ChatMember>>) change -> requestImmediateRefresh()
);
requestImmediateRefresh();
}
@@ -171,17 +170,6 @@ public final class StationMapBridge {
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) {
String normalizedOwnLocator6 = normalizeLocator6(ownLocator6);
@@ -214,28 +202,20 @@ public final class StationMapBridge {
PathAnalysisResult.loading(normalizedOwnLocator6, normalizedTargetLocator6, targetCallsignRaw)
);
ChatMember selectedMember = resolveBestChatMember(targetCallsignRaw);
pathAnalysisExecutor.submit(() -> {
PathAnalysisResult result = buildPathAnalysisResult(normalizedOwnLocator6, selectedSnapshot);
chatController.getReachabilityService().requestPathAnalysisForMap(
selectedMember,
selectedSnapshot,
result -> {
Platform.runLater(() -> {
if (generation != pathAnalysisGeneration.get()) {
return;
}
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() {
pathAnalysisGeneration.incrementAndGet();
pathAnalysisExecutor.shutdownNow();
}
private void handleMapCallsignSelection(String callSignRaw) {
@@ -306,6 +286,43 @@ public final class StationMapBridge {
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) {
if (selectedSnapshot == null) {
@@ -359,5 +376,12 @@ public final class StationMapBridge {
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.model.ChatPreferences;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -49,7 +48,6 @@ public final class StationMapView {
private final Stage stage = new Stage();
private final WebView webView = new WebView();
private final WebEngine webEngine = webView.getEngine();
private TileProxyServer tileProxyServer;
private Scene scene;
private BorderPane rootPane;
@@ -129,11 +127,6 @@ public final class StationMapView {
public StationMapView(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();
initializeWebView();
}
@@ -634,8 +627,7 @@ public final class StationMapView {
}
});
int proxyPort = tileProxyServer != null ? tileProxyServer.getPort() : 0;
webEngine.loadContent(MapHtmlResources.createStationMapHtml(proxyPort));
webEngine.loadContent(MapHtmlResources.createStationMapHtml());
}
private void requestMapInvalidateSize() {
@@ -1101,7 +1093,6 @@ public final class StationMapView {
webEngine.executeScript(script);
} catch (Exception exception) {
System.err.println("[StationMap] executeScript failed: " + exception.getMessage());
exception.printStackTrace();
}
}
-5
View File
@@ -2366,8 +2366,3 @@ OZ1FDH;Claus;JO55QX;StringProperty [value: null]; wkd true; wkd144 true; wkd432f
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
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