Feature/map view and filters (#40)
Nightly Runtime Artifacts / Build Windows ZIP (push) Has been cancelled
Nightly Runtime Artifacts / Build Linux AppImage (push) Has been cancelled
Nightly Runtime Artifacts / Build Debian package (push) Has been cancelled
Nightly Runtime Artifacts / Build Fedora package (push) Has been cancelled
Nightly Runtime Artifacts / Build Arch Linux package (push) Has been cancelled
Nightly Runtime Artifacts / Build Flatpak (push) Has been cancelled
Nightly Runtime Artifacts / Build macOS DMG (macos-15-intel) (push) Has been cancelled
Nightly Runtime Artifacts / Build macOS DMG (macos-latest) (push) Has been cancelled
Nightly Runtime Artifacts / Build Windows ZIP (push) Has been cancelled
Nightly Runtime Artifacts / Build Linux AppImage (push) Has been cancelled
Nightly Runtime Artifacts / Build Debian package (push) Has been cancelled
Nightly Runtime Artifacts / Build Fedora package (push) Has been cancelled
Nightly Runtime Artifacts / Build Arch Linux package (push) Has been cancelled
Nightly Runtime Artifacts / Build Flatpak (push) Has been cancelled
Nightly Runtime Artifacts / Build macOS DMG (macos-15-intel) (push) Has been cancelled
Nightly Runtime Artifacts / Build macOS DMG (macos-latest) (push) Has been cancelled
* Added a map to show where other stn are // refactored message adding to tables for performance, max 30.000 msg now * debugging map failure * debugging map failure * debugging map failure * fix Pipeline Modules n map Linux --------- Co-authored-by: Marc Froehlich <praktimarc@gmail.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
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.
|
||||
*
|
||||
@@ -16,7 +21,21 @@ public final class MapHtmlResources {
|
||||
private MapHtmlResources() {
|
||||
}
|
||||
|
||||
public static String createStationMapHtml() {
|
||||
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");
|
||||
|
||||
return """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -24,10 +43,9 @@ public final class MapHtmlResources {
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KST4Contest Station Map</title>
|
||||
<link rel="stylesheet"
|
||||
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin="">
|
||||
<style>
|
||||
""" + leafletCss + """
|
||||
</style>
|
||||
<style>
|
||||
:root {
|
||||
--map-background: #ede9df;
|
||||
@@ -196,9 +214,10 @@ public final class MapHtmlResources {
|
||||
<body class="kst-theme-light">
|
||||
<div id="map"></div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
<script>
|
||||
""" + leafletJs + """
|
||||
</script>
|
||||
<script>window._kstTileProxyPort=__TILE_PROXY_PORT__;</script>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -336,7 +355,12 @@ public final class MapHtmlResources {
|
||||
|
||||
function init() {
|
||||
if (map) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof L === 'undefined') {
|
||||
jsError('Leaflet is not loaded. Station map cannot initialize.');
|
||||
return false;
|
||||
}
|
||||
|
||||
applyThemeClass();
|
||||
@@ -347,10 +371,13 @@ public final class MapHtmlResources {
|
||||
|
||||
jsLog('Leaflet map initialized');
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 18,
|
||||
attribution: '© OpenStreetMap'
|
||||
}).addTo(map);
|
||||
L.tileLayer(
|
||||
'http://127.0.0.1:' + window._kstTileProxyPort + '/tiles/{s}/{z}/{x}/{y}.png',
|
||||
{
|
||||
maxZoom: 18,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
map.createPane('beamPane');
|
||||
map.getPane('beamPane').style.zIndex = 410;
|
||||
@@ -385,6 +412,7 @@ public final class MapHtmlResources {
|
||||
|
||||
notifyMapReady();
|
||||
notifyViewport();
|
||||
return true;
|
||||
}
|
||||
|
||||
function invalidateSize() {
|
||||
@@ -468,13 +496,17 @@ public final class MapHtmlResources {
|
||||
}
|
||||
|
||||
function setHome(lat, lon, zoom) {
|
||||
init();
|
||||
if (!init()) {
|
||||
return;
|
||||
}
|
||||
jsLog('setHome lat=' + lat + ' lon=' + lon + ' zoom=' + zoom);
|
||||
map.setView([lat, lon], zoom);
|
||||
}
|
||||
|
||||
function setStations(stationsJson) {
|
||||
init();
|
||||
if (!init()) {
|
||||
return;
|
||||
}
|
||||
|
||||
stationLayer.clearLayers();
|
||||
markersByCallsignRaw = {};
|
||||
@@ -503,7 +535,9 @@ public final class MapHtmlResources {
|
||||
}
|
||||
|
||||
function setBeam(beamJson) {
|
||||
init();
|
||||
if (!init()) {
|
||||
return;
|
||||
}
|
||||
beamLayer.clearLayers();
|
||||
|
||||
if (!beamJson || beamJson === 'null') {
|
||||
@@ -530,7 +564,9 @@ public final class MapHtmlResources {
|
||||
}
|
||||
|
||||
function setConnection(connectionJson) {
|
||||
init();
|
||||
if (!init()) {
|
||||
return;
|
||||
}
|
||||
connectionLayer.clearLayers();
|
||||
|
||||
if (!connectionJson || connectionJson === 'null') {
|
||||
@@ -555,7 +591,9 @@ public final class MapHtmlResources {
|
||||
}
|
||||
|
||||
function setProfileHoverPoint(point) {
|
||||
init();
|
||||
if (!init()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (profileHoverMarker) {
|
||||
map.removeLayer(profileHoverMarker);
|
||||
@@ -584,7 +622,9 @@ public final class MapHtmlResources {
|
||||
}
|
||||
|
||||
function setGrid(gridJson) {
|
||||
init();
|
||||
if (!init()) {
|
||||
return;
|
||||
}
|
||||
gridLayer.clearLayers();
|
||||
|
||||
const cells = JSON.parse(gridJson);
|
||||
@@ -670,6 +710,6 @@ public final class MapHtmlResources {
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
""".replace("__TILE_PROXY_PORT__", String.valueOf(tileProxyPort));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ 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;
|
||||
@@ -48,6 +49,7 @@ 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;
|
||||
@@ -127,6 +129,11 @@ 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();
|
||||
}
|
||||
@@ -627,7 +634,8 @@ public final class StationMapView {
|
||||
}
|
||||
});
|
||||
|
||||
webEngine.loadContent(MapHtmlResources.createStationMapHtml());
|
||||
int proxyPort = tileProxyServer != null ? tileProxyServer.getPort() : 0;
|
||||
webEngine.loadContent(MapHtmlResources.createStationMapHtml(proxyPort));
|
||||
}
|
||||
|
||||
private void requestMapInvalidateSize() {
|
||||
@@ -1093,6 +1101,7 @@ public final class StationMapView {
|
||||
webEngine.executeScript(script);
|
||||
} catch (Exception exception) {
|
||||
System.err.println("[StationMap] executeScript failed: " + exception.getMessage());
|
||||
exception.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1443,4 +1452,4 @@ public final class StationMapView {
|
||||
|
||||
return storedSize[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package kst4contest.view.map;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* Minimal HTTP server that proxies OSM tile requests via Java's HttpClient,
|
||||
* so JavaFX WebView only needs to connect to localhost (avoids WebKit SSL/sandbox
|
||||
* issues in AppImage and Flatpak packaging). Uses only java.base + java.net.http.
|
||||
*/
|
||||
final class TileProxyServer {
|
||||
|
||||
private static final int CACHE_MAX = 512;
|
||||
private static final String USER_AGENT = "kst4contest/1.0 amateur-radio-contest-tool";
|
||||
|
||||
private final ServerSocket serverSocket;
|
||||
private final ExecutorService executor;
|
||||
private final HttpClient httpClient;
|
||||
private final Map<String, byte[]> cache;
|
||||
|
||||
TileProxyServer() throws IOException {
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
|
||||
this.cache = Collections.synchronizedMap(new LinkedHashMap<>(CACHE_MAX, 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<String, byte[]> eldest) {
|
||||
return size() > CACHE_MAX;
|
||||
}
|
||||
});
|
||||
|
||||
this.serverSocket = new ServerSocket(0, 16, InetAddress.getByName("127.0.0.1"));
|
||||
|
||||
this.executor = Executors.newFixedThreadPool(4, r -> {
|
||||
Thread t = new Thread(r, "tile-proxy");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
executor.submit(this::acceptLoop);
|
||||
}
|
||||
|
||||
int getPort() {
|
||||
return serverSocket.getLocalPort();
|
||||
}
|
||||
|
||||
void stop() {
|
||||
try { serverSocket.close(); } catch (IOException ignored) {}
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
private void acceptLoop() {
|
||||
while (!serverSocket.isClosed()) {
|
||||
try {
|
||||
Socket client = serverSocket.accept();
|
||||
executor.submit(() -> handleClient(client));
|
||||
} catch (IOException e) {
|
||||
if (!serverSocket.isClosed()) {
|
||||
System.err.println("[TileProxy] accept error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleClient(Socket client) {
|
||||
try (client;
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
|
||||
OutputStream out = client.getOutputStream()) {
|
||||
|
||||
String requestLine = in.readLine();
|
||||
if (requestLine == null || !requestLine.startsWith("GET ")) {
|
||||
sendError(out, 400, "Bad Request");
|
||||
return;
|
||||
}
|
||||
|
||||
// Drain headers
|
||||
String line;
|
||||
while ((line = in.readLine()) != null && !line.isEmpty()) { /* skip */ }
|
||||
|
||||
// Parse: GET /tiles/{s}/{z}/{x}/{y}.png HTTP/1.1
|
||||
String[] parts = requestLine.split(" ");
|
||||
if (parts.length < 2) { sendError(out, 400, "Bad Request"); return; }
|
||||
String path = parts[1];
|
||||
|
||||
String[] segments = path.split("/");
|
||||
// segments: ["", "tiles", s, z, x, "y.png"]
|
||||
if (segments.length != 6
|
||||
|| !segments[2].matches("[abc]")
|
||||
|| !segments[3].matches("\\d{1,2}")
|
||||
|| !segments[4].matches("\\d+")
|
||||
|| !segments[5].matches("\\d+\\.png")) {
|
||||
sendError(out, 404, "Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
String cacheKey = segments[2] + "/" + segments[3] + "/" + segments[4] + "/" + segments[5];
|
||||
byte[] tileData = cache.get(cacheKey);
|
||||
|
||||
if (tileData == null) {
|
||||
String tileUrl = "https://" + segments[2] + ".tile.openstreetmap.org/"
|
||||
+ segments[3] + "/" + segments[4] + "/" + segments[5];
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(tileUrl))
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.timeout(Duration.ofSeconds(15))
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
sendError(out, 502, "OSM returned " + response.statusCode());
|
||||
return;
|
||||
}
|
||||
|
||||
tileData = response.body();
|
||||
cache.put(cacheKey, tileData);
|
||||
}
|
||||
|
||||
String header = "HTTP/1.1 200 OK\r\n"
|
||||
+ "Content-Type: image/png\r\n"
|
||||
+ "Content-Length: " + tileData.length + "\r\n"
|
||||
+ "Cache-Control: max-age=86400\r\n"
|
||||
+ "Connection: close\r\n"
|
||||
+ "\r\n";
|
||||
out.write(header.getBytes());
|
||||
out.write(tileData);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Exception e) {
|
||||
System.err.println("[TileProxy] error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendError(OutputStream out, int code, String message) throws IOException {
|
||||
byte[] body = message.getBytes();
|
||||
String response = "HTTP/1.1 " + code + " " + message + "\r\n"
|
||||
+ "Content-Length: " + body.length + "\r\n"
|
||||
+ "Connection: close\r\n"
|
||||
+ "\r\n";
|
||||
out.write(response.getBytes());
|
||||
out.write(body);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ module praktiKST {
|
||||
requires jdk.jsobject;
|
||||
requires java.net.http;
|
||||
requires java.desktop;
|
||||
requires jdk.crypto.ec;
|
||||
exports kst4contest.controller.interfaces;
|
||||
exports kst4contest.controller;
|
||||
exports kst4contest.locatorUtils;
|
||||
|
||||
Reference in New Issue
Block a user