mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-18 21:34:03 +02:00
Optimize station map refresh performance
This commit is contained in:
1 parent
df70cb1943
commit
3e8457eede
12 files changed
+625
-81
No files matched your search
@@ -150,6 +150,10 @@ CR/LF framing, XML framing, ports/transports, callsign normalization and frequen
|
||||
- Map reset clears the selected target without changing zoom unless explicitly redesigned.
|
||||
- **Group nearby stations** re-renders only the existing station-marker layer from JavaScript `stationData`. It must not reload the WebView, tiles or station data, request a new controller snapshot, or change zoom, viewport or selection.
|
||||
- Base-callsign aggregation into one geographical marker happens before screen-based clustering. Disabling clustering displays each resulting positionable map station individually but never splits active variants of the same normalised base callsign into separate geographical markers.
|
||||
- A hidden Station Map is not continuously refreshed. Relevant background changes mark it dirty; the next `showWindow()` performs one current full refresh before normal visible-window updates resume.
|
||||
- Maidenhead grid construction depends only on viewport bounds, zoom and WebView size. Ordinary station-data refreshes must not rebuild or serialise the grid when those inputs are unchanged.
|
||||
- Panning updates viewport and grid state through `notifyViewport()` without rebuilding station markers or screen-based clusters. Zoom changes, station-data changes and clustering toggles still rebuild the marker layer.
|
||||
- Debug file logging reports Station Map refreshes only when total FX-thread refresh time reaches 100 ms. The diagnostic separates snapshot and render time and includes visible-member and station counts.
|
||||
- Station selection preserves the established `/cq callsign` prefill behaviour.
|
||||
- Sending without an explicitly selected send category preserves the established Main-category fallback unless explicitly changed.
|
||||
|
||||
|
||||
@@ -2198,12 +2198,6 @@ public class MessageBusManagementThread extends Thread {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
|
||||
if (!On4KstProtocol.isInternalDxqResponse(
|
||||
messageTextRaw.getMessageText())) {
|
||||
LOGGER.log(Level.FINE, "ON4KST RX: {0}",
|
||||
messageTextRaw.getMessageText());
|
||||
}
|
||||
|
||||
try {
|
||||
processRXMessage23001(messageTextRaw);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -27,7 +27,6 @@ public class Utils4KST {
|
||||
public String time_generateCurrenthhmmZTimeStringForClusterMessage() {
|
||||
|
||||
OffsetDateTime currentTimeInUtc = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
// System.out.println("Utils generated current time " + currentTimeInUtc + " --> " + currentTimeInUtc.format(DateTimeFormatter.ofPattern("HHmm"))+"Z");
|
||||
return currentTimeInUtc.format(DateTimeFormatter.ofPattern("HHmm"))+"Z";
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ public class DirectionUtils {
|
||||
|
||||
// Check if distance exceeds my setted maximum range
|
||||
if (distanceFromMeToLocSender > maxRangeKm) {
|
||||
System.out.println("too far, " + distanceFromMeToLocSender + " km");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -795,8 +795,8 @@ public final class MapHtmlResources {
|
||||
/**
|
||||
* Re-renders station markers for the current zoom and viewport.
|
||||
*
|
||||
* This is called when stations arrive from Java and after zoom/move
|
||||
* events, because screen-grid clusters depend on the current viewport.
|
||||
* This is called when stations arrive from Java, clustering changes and
|
||||
* after zoom events. Panning keeps the existing screen-grid clusters.
|
||||
*/
|
||||
function renderStationMarkers() {
|
||||
if (!map || !stationLayer) {
|
||||
@@ -941,7 +941,6 @@ public final class MapHtmlResources {
|
||||
});
|
||||
|
||||
map.on('moveend', function () {
|
||||
renderStationMarkers();
|
||||
notifyViewport();
|
||||
});
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import kst4contest.model.Band;
|
||||
import java.util.function.Predicate;
|
||||
@@ -34,6 +36,9 @@ import java.util.function.Predicate;
|
||||
*/
|
||||
public final class StationMapBridge {
|
||||
|
||||
/** Application logger for map refresh diagnostics. */
|
||||
private static final Logger LOGGER =
|
||||
Logger.getLogger(StationMapBridge.class.getName());
|
||||
|
||||
private final AtomicLong pathAnalysisGeneration = new AtomicLong(0);
|
||||
|
||||
@@ -48,6 +53,9 @@ public final class StationMapBridge {
|
||||
|
||||
|
||||
private final MapCallsignRawSnapshotBuilder snapshotBuilder = new MapCallsignRawSnapshotBuilder();
|
||||
/** Tracks one deferred refresh while the map window is hidden. */
|
||||
private final StationMapPerformanceSupport.RefreshState refreshState =
|
||||
new StationMapPerformanceSupport.RefreshState();
|
||||
|
||||
|
||||
private String lastPathAnalysisRequestSignature = "";
|
||||
@@ -131,10 +139,14 @@ public final class StationMapBridge {
|
||||
|
||||
public void showWindow() {
|
||||
stationMapView.showWindow();
|
||||
requestImmediateRefresh();
|
||||
if (refreshState.consumeDirtyOnShow()) {
|
||||
requestImmediateRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void hideWindow() {
|
||||
refreshCoalescer.stop();
|
||||
refreshState.markDirty();
|
||||
stationMapView.hideWindow();
|
||||
}
|
||||
|
||||
@@ -184,19 +196,35 @@ public final class StationMapBridge {
|
||||
}
|
||||
|
||||
private void scheduleRefresh() {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
final Runnable scheduleAction = () -> {
|
||||
if (!stationMapView.isShowing()) {
|
||||
refreshCoalescer.stop();
|
||||
refreshState.markDirty();
|
||||
return;
|
||||
}
|
||||
refreshCoalescer.playFromStart();
|
||||
};
|
||||
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
scheduleAction.run();
|
||||
} else {
|
||||
Platform.runLater(() -> refreshCoalescer.playFromStart());
|
||||
Platform.runLater(scheduleAction);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshNow() {
|
||||
List<ChatMember> visibleChatMembers = new ArrayList<>(chatController.getLst_chatMemberSortedFilteredList());
|
||||
ChatMember selectedChatMember = chatController.getScoreService().getSelectedChatMember();
|
||||
EnumSet<Band> selectedBands = chatController.getReachabilityService().getEnabledStationBands();
|
||||
if (!refreshState.shouldRefreshNow(stationMapView.isShowing())) {
|
||||
refreshCoalescer.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
List<MapCallsignRawSnapshot> snapshots = snapshotBuilder.buildSnapshots(
|
||||
final long refreshStart = System.nanoTime();
|
||||
final long snapshotStart = refreshStart;
|
||||
final List<ChatMember> visibleChatMembers = new ArrayList<>(chatController.getLst_chatMemberSortedFilteredList());
|
||||
final ChatMember selectedChatMember = chatController.getScoreService().getSelectedChatMember();
|
||||
final EnumSet<Band> selectedBands = chatController.getReachabilityService().getEnabledStationBands();
|
||||
|
||||
final List<MapCallsignRawSnapshot> snapshots = snapshotBuilder.buildSnapshots(
|
||||
visibleChatMembers,
|
||||
selectedChatMember,
|
||||
selectedBands
|
||||
@@ -204,17 +232,19 @@ public final class StationMapBridge {
|
||||
|
||||
MapCallsignRawSnapshot selectedSnapshot = null;
|
||||
if (selectedChatMember != null && selectedChatMember.getCallSignRaw() != null) {
|
||||
String selectedCallsignRaw = normalizeCallsignRaw(selectedChatMember.getCallSignRaw());
|
||||
final String selectedCallsignRaw = normalizeCallsignRaw(selectedChatMember.getCallSignRaw());
|
||||
selectedSnapshot = snapshots.stream()
|
||||
.filter(snapshot -> snapshot.callSignRaw().equals(selectedCallsignRaw))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
final long snapshotNanos = System.nanoTime() - snapshotStart;
|
||||
|
||||
boolean filteredViewActive = visibleChatMembers.size() < chatController.getLst_chatMemberList().size();
|
||||
final boolean filteredViewActive = visibleChatMembers.size() < chatController.getLst_chatMemberList().size();
|
||||
|
||||
ChatPreferences preferences = chatController.getChatPreferences();
|
||||
final ChatPreferences preferences = chatController.getChatPreferences();
|
||||
|
||||
final long renderStart = System.nanoTime();
|
||||
stationMapView.refreshMap(
|
||||
snapshots,
|
||||
selectedSnapshot,
|
||||
@@ -224,8 +254,22 @@ public final class StationMapBridge {
|
||||
preferences.getStn_maxQRBDefault(),
|
||||
filteredViewActive
|
||||
);
|
||||
final long renderNanos = System.nanoTime() - renderStart;
|
||||
|
||||
requestPathAnalysisAsync(preferences.getStn_loginLocatorMainCat(), selectedSnapshot);
|
||||
|
||||
final long totalNanos = System.nanoTime() - refreshStart;
|
||||
if (preferences.isMessageHandling_debugModeToFileEnabled()
|
||||
&& StationMapPerformanceSupport.isSlowRefresh(totalNanos)
|
||||
&& LOGGER.isLoggable(Level.FINE)) {
|
||||
LOGGER.log(Level.FINE,
|
||||
StationMapPerformanceSupport.formatSlowRefreshMessage(
|
||||
totalNanos,
|
||||
snapshotNanos,
|
||||
renderNanos,
|
||||
visibleChatMembers.size(),
|
||||
snapshots.size()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,8 +344,6 @@ public final class StationMapBridge {
|
||||
}
|
||||
|
||||
private void handleMapCallsignSelection(String callSignRaw) {
|
||||
System.out.println("########################### map selected callsign " + callSignRaw);
|
||||
|
||||
ChatMember resolved = resolveBestChatMember(callSignRaw);
|
||||
if (resolved == null) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package kst4contest.view.map;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** Small JavaFX-independent state helpers for station-map refresh decisions. */
|
||||
final class StationMapPerformanceSupport {
|
||||
|
||||
/** Minimum duration for a slow-refresh diagnostic. */
|
||||
private static final long SLOW_NANOS =
|
||||
TimeUnit.MILLISECONDS.toNanos(100L);
|
||||
|
||||
private StationMapPerformanceSupport() {
|
||||
}
|
||||
|
||||
/* default */ static void runDebugIfEnabled(
|
||||
final boolean enabled,
|
||||
final Runnable operation
|
||||
) {
|
||||
Objects.requireNonNull(operation, "operation");
|
||||
if (enabled) {
|
||||
operation.run();
|
||||
}
|
||||
}
|
||||
|
||||
/* default */ static boolean isSlowRefresh(final long totalNanos) {
|
||||
return totalNanos >= SLOW_NANOS;
|
||||
}
|
||||
|
||||
/* default */ static String formatSlowRefreshMessage(
|
||||
final long totalNanos,
|
||||
final long snapshotNanos,
|
||||
final long renderNanos,
|
||||
final int visibleCount,
|
||||
final int stationCount
|
||||
) {
|
||||
return "Station map refresh slow: total="
|
||||
+ TimeUnit.NANOSECONDS.toMillis(totalNanos)
|
||||
+ " ms, snapshots="
|
||||
+ TimeUnit.NANOSECONDS.toMillis(snapshotNanos)
|
||||
+ " ms, render="
|
||||
+ TimeUnit.NANOSECONDS.toMillis(renderNanos)
|
||||
+ " ms, visibleMembers="
|
||||
+ visibleCount
|
||||
+ ", stations="
|
||||
+ stationCount;
|
||||
}
|
||||
|
||||
/** Tracks deferred refresh work while the map window is hidden. */
|
||||
/* default */ static final class RefreshState {
|
||||
/** Whether current application state still needs to reach the map. */
|
||||
private boolean dirty = true;
|
||||
|
||||
/* default */ boolean shouldRefreshNow(final boolean visible) {
|
||||
dirty = !visible;
|
||||
return visible;
|
||||
}
|
||||
|
||||
/* default */ void markDirty() {
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
/* default */ boolean consumeDirtyOnShow() {
|
||||
final boolean refreshRequired = dirty;
|
||||
dirty = false;
|
||||
return refreshRequired;
|
||||
}
|
||||
}
|
||||
|
||||
/** Complete set of inputs that can change the visible grid rendering. */
|
||||
/* default */ record GridRenderInput(
|
||||
double southLatitude,
|
||||
double westLongitude,
|
||||
double northLatitude,
|
||||
double eastLongitude,
|
||||
int zoom,
|
||||
double viewportWidth,
|
||||
double viewportHeight
|
||||
) {
|
||||
}
|
||||
|
||||
/** Rejects duplicate grid inputs before cell construction starts. */
|
||||
/* default */ static final class GridRenderState {
|
||||
/** Inputs used for the last planned grid rendering. */
|
||||
private GridRenderInput lastInput;
|
||||
|
||||
/* default */ boolean shouldRender(final GridRenderInput input) {
|
||||
Objects.requireNonNull(input, "input");
|
||||
final boolean renderRequired = !input.equals(lastInput);
|
||||
if (renderRequired) {
|
||||
lastInput = input;
|
||||
}
|
||||
return renderRequired;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
@@ -44,6 +46,10 @@ import javafx.scene.layout.HBox;
|
||||
*/
|
||||
public final class StationMapView {
|
||||
|
||||
/** Application logger for map failures that must remain visible. */
|
||||
private static final Logger LOGGER =
|
||||
Logger.getLogger(StationMapView.class.getName());
|
||||
|
||||
/**
|
||||
* Enables verbose map debug output.
|
||||
*
|
||||
@@ -130,6 +136,9 @@ public final class StationMapView {
|
||||
private String lastRenderedBeamJson = "";
|
||||
private String lastRenderedConnectionJson = "";
|
||||
private String lastRenderedGridJson = "";
|
||||
/** Rejects unchanged viewport inputs before expensive grid construction. */
|
||||
private final StationMapPerformanceSupport.GridRenderState gridRenderState =
|
||||
new StationMapPerformanceSupport.GridRenderState();
|
||||
|
||||
/**
|
||||
* Prevent periodic refreshes from panning the map back to the selected station.
|
||||
@@ -195,7 +204,8 @@ public final class StationMapView {
|
||||
try {
|
||||
tileProxyServer = new TileProxyServer();
|
||||
} catch (IOException e) {
|
||||
System.err.println("[StationMap] tile proxy failed to start: " + e.getMessage());
|
||||
LOGGER.log(Level.WARNING,
|
||||
"Station map tile proxy failed to start", e);
|
||||
}
|
||||
initializeUi();
|
||||
initializeWebView();
|
||||
@@ -362,8 +372,14 @@ public final class StationMapView {
|
||||
webView.setFocusTraversable(true);
|
||||
webView.setPickOnBounds(true);
|
||||
|
||||
webView.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> logWebViewMouseEvent("MOUSE_PRESSED", event));
|
||||
webView.addEventHandler(MouseEvent.MOUSE_RELEASED, event -> logWebViewMouseEvent("MOUSE_RELEASED", event));
|
||||
if (MAP_DEBUG_LOGGING) {
|
||||
webView.addEventHandler(
|
||||
MouseEvent.MOUSE_PRESSED,
|
||||
event -> logWebViewMouseEvent("MOUSE_PRESSED", event));
|
||||
webView.addEventHandler(
|
||||
MouseEvent.MOUSE_RELEASED,
|
||||
event -> logWebViewMouseEvent("MOUSE_RELEASED", event));
|
||||
}
|
||||
webView.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> handleWebViewClick(event));
|
||||
|
||||
webView.addEventHandler(ScrollEvent.SCROLL, event -> {
|
||||
@@ -374,8 +390,10 @@ public final class StationMapView {
|
||||
+ " deltaY=" + event.getDeltaY());
|
||||
}
|
||||
|
||||
InteractiveTarget target = inspectInteractiveTarget(event.getX(), event.getY());
|
||||
if (MAP_DEBUG_LOGGING) {
|
||||
final InteractiveTarget target = inspectInteractiveTarget(
|
||||
event.getX(),
|
||||
event.getY());
|
||||
System.out.println("[StationMap FX] inspect scroll -> " + target);
|
||||
}
|
||||
|
||||
@@ -784,19 +802,21 @@ public final class StationMapView {
|
||||
try {
|
||||
Object result = webEngine.executeScript("window.kstMapApi.getViewportState();");
|
||||
if (result == null) {
|
||||
System.err.println("[StationMap FX] getViewportState returned null");
|
||||
LOGGER.warning("Station map getViewportState returned null");
|
||||
return;
|
||||
}
|
||||
|
||||
String raw = result.toString();
|
||||
if (raw.isBlank()) {
|
||||
System.err.println("[StationMap FX] getViewportState returned blank");
|
||||
LOGGER.warning("Station map getViewportState returned blank");
|
||||
return;
|
||||
}
|
||||
|
||||
String[] parts = raw.split("\\|");
|
||||
if (parts.length != 5) {
|
||||
System.err.println("[StationMap FX] getViewportState unexpected format: " + raw);
|
||||
LOGGER.log(Level.WARNING,
|
||||
"Station map getViewportState returned unexpected format: {0}",
|
||||
raw);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -806,15 +826,19 @@ public final class StationMapView {
|
||||
viewportEastLon = Double.parseDouble(parts[3]);
|
||||
viewportZoom = (int) Math.round(Double.parseDouble(parts[4]));
|
||||
|
||||
System.out.println("[StationMap FX] pulled viewport south=" + viewportSouthLat
|
||||
+ " west=" + viewportWestLon
|
||||
+ " north=" + viewportNorthLat
|
||||
+ " east=" + viewportEastLon
|
||||
+ " zoom=" + viewportZoom);
|
||||
if (MAP_DEBUG_LOGGING) {
|
||||
System.out.println("[StationMap FX] pulled viewport south=" + viewportSouthLat
|
||||
+ " west=" + viewportWestLon
|
||||
+ " north=" + viewportNorthLat
|
||||
+ " east=" + viewportEastLon
|
||||
+ " zoom=" + viewportZoom);
|
||||
}
|
||||
|
||||
renderGridIfViewportKnown();
|
||||
} catch (Exception exception) {
|
||||
System.err.println("[StationMap FX] pullViewportFromJsAndRedrawGrid failed: " + exception.getMessage());
|
||||
LOGGER.log(Level.WARNING,
|
||||
"Station map viewport pull and grid redraw failed",
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -854,28 +878,37 @@ public final class StationMapView {
|
||||
|
||||
|
||||
|
||||
private void handleWebViewClick(MouseEvent event) {
|
||||
private void handleWebViewClick(final MouseEvent event) {
|
||||
logWebViewMouseEvent("MOUSE_CLICKED", event);
|
||||
|
||||
InteractiveTarget target = inspectInteractiveTarget(event.getX(), event.getY());
|
||||
System.out.println("[StationMap FX] inspect click -> " + target);
|
||||
final InteractiveTarget target = inspectInteractiveTarget(
|
||||
event.getX(), event.getY());
|
||||
if (MAP_DEBUG_LOGGING) {
|
||||
System.out.println("[StationMap FX] inspect click -> " + target);
|
||||
}
|
||||
|
||||
switch (target.kind()) {
|
||||
case STATION -> {
|
||||
if (target.callSignRaw() != null && !target.callSignRaw().isBlank() && onCallsignRawSelected != null) {
|
||||
System.out.println("[StationMap FX] selecting station " + target.callSignRaw());
|
||||
if (MAP_DEBUG_LOGGING) {
|
||||
System.out.println("[StationMap FX] selecting station " + target.callSignRaw());
|
||||
}
|
||||
onCallsignRawSelected.accept(target.callSignRaw());
|
||||
event.consume();
|
||||
}
|
||||
}
|
||||
case ZOOM_IN -> {
|
||||
System.out.println("[StationMap FX] zoom in click routed by JavaFX");
|
||||
if (MAP_DEBUG_LOGGING) {
|
||||
System.out.println("[StationMap FX] zoom in click routed by JavaFX");
|
||||
}
|
||||
executeMapScriptSafely("window.kstMapApi.zoomIn();");
|
||||
requestViewportPullFromJs();
|
||||
event.consume();
|
||||
}
|
||||
case ZOOM_OUT -> {
|
||||
System.out.println("[StationMap FX] zoom out click routed by JavaFX");
|
||||
if (MAP_DEBUG_LOGGING) {
|
||||
System.out.println("[StationMap FX] zoom out click routed by JavaFX");
|
||||
}
|
||||
executeMapScriptSafely("window.kstMapApi.zoomOut();");
|
||||
requestViewportPullFromJs();
|
||||
event.consume();
|
||||
@@ -887,18 +920,27 @@ public final class StationMapView {
|
||||
}
|
||||
}
|
||||
|
||||
private void logWebViewMouseEvent(String type, MouseEvent event) {
|
||||
System.out.println("[StationMap FX] " + type
|
||||
+ " x=" + (int) event.getX()
|
||||
+ " y=" + (int) event.getY()
|
||||
+ " target=" + event.getTarget().getClass().getSimpleName()
|
||||
+ " button=" + event.getButton());
|
||||
|
||||
probeDomElementAt(event.getX(), event.getY());
|
||||
private void logWebViewMouseEvent(
|
||||
final String type,
|
||||
final MouseEvent event
|
||||
) {
|
||||
StationMapPerformanceSupport.runDebugIfEnabled(
|
||||
MAP_DEBUG_LOGGING,
|
||||
() -> {
|
||||
System.out.println("[StationMap FX] " + type
|
||||
+ " x=" + (int) event.getX()
|
||||
+ " y=" + (int) event.getY()
|
||||
+ " target=" + event.getTarget().getClass().getSimpleName()
|
||||
+ " button=" + event.getButton());
|
||||
probeDomElementAt(event.getX(), event.getY());
|
||||
});
|
||||
}
|
||||
|
||||
private void probeDomElementAt(double x, double y) {
|
||||
if (!mapReady) {
|
||||
private void probeDomElementAt(
|
||||
final double xCoordinate,
|
||||
final double yCoordinate
|
||||
) {
|
||||
if (!MAP_DEBUG_LOGGING || !mapReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -921,13 +963,15 @@ public final class StationMapView {
|
||||
}
|
||||
return "tag=" + el.tagName + " class=" + cls + " id=" + id + " text=" + text;
|
||||
})();
|
||||
""", x, y);
|
||||
""", xCoordinate, yCoordinate);
|
||||
|
||||
try {
|
||||
Object result = webEngine.executeScript(script);
|
||||
System.out.println("[StationMap FX] elementFromPoint -> " + result);
|
||||
} catch (Exception exception) {
|
||||
System.err.println("[StationMap FX] elementFromPoint failed: " + exception.getMessage());
|
||||
LOGGER.log(Level.WARNING,
|
||||
"Station map elementFromPoint probe failed",
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -963,7 +1007,9 @@ public final class StationMapView {
|
||||
default -> new InteractiveTarget(InteractiveKind.NONE, "", tag, cssClass, text);
|
||||
};
|
||||
} catch (Exception exception) {
|
||||
System.err.println("[StationMap FX] inspectInteractiveTarget failed: " + exception.getMessage());
|
||||
LOGGER.log(Level.WARNING,
|
||||
"Station map interactive-target inspection failed",
|
||||
exception);
|
||||
return InteractiveTarget.none();
|
||||
}
|
||||
}
|
||||
@@ -1185,7 +1231,6 @@ public final class StationMapView {
|
||||
renderStations();
|
||||
renderBeam();
|
||||
renderConnectionLine();
|
||||
renderGridIfViewportKnown();
|
||||
focusSelectedStationOnlyWhenSelectionChanged();
|
||||
}
|
||||
|
||||
@@ -1269,10 +1314,23 @@ public final class StationMapView {
|
||||
return;
|
||||
}
|
||||
|
||||
double viewportWidthPx = Math.max(1.0, webView.getWidth());
|
||||
double viewportHeightPx = Math.max(1.0, webView.getHeight());
|
||||
final double viewportWidthPx = Math.max(1.0, webView.getWidth());
|
||||
final double viewportHeightPx = Math.max(1.0, webView.getHeight());
|
||||
|
||||
MaidenheadGridRenderPlanner.GridRenderPlan renderPlan = MaidenheadGridRenderPlanner.createPlan(
|
||||
final StationMapPerformanceSupport.GridRenderInput renderInput =
|
||||
new StationMapPerformanceSupport.GridRenderInput(
|
||||
viewportSouthLat,
|
||||
viewportWestLon,
|
||||
viewportNorthLat,
|
||||
viewportEastLon,
|
||||
viewportZoom,
|
||||
viewportWidthPx,
|
||||
viewportHeightPx);
|
||||
if (!gridRenderState.shouldRender(renderInput)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final MaidenheadGridRenderPlanner.GridRenderPlan renderPlan = MaidenheadGridRenderPlanner.createPlan(
|
||||
viewportZoom,
|
||||
viewportSouthLat,
|
||||
viewportWestLon,
|
||||
@@ -1282,7 +1340,7 @@ public final class StationMapView {
|
||||
viewportHeightPx
|
||||
);
|
||||
|
||||
List<MaidenheadGridUtils.GridCell> visibleGridCells = MaidenheadGridUtils.buildVisibleCells(
|
||||
final List<MaidenheadGridUtils.GridCell> visibleGridCells = MaidenheadGridUtils.buildVisibleCells(
|
||||
viewportSouthLat,
|
||||
viewportWestLon,
|
||||
viewportNorthLat,
|
||||
@@ -1290,13 +1348,15 @@ public final class StationMapView {
|
||||
renderPlan.precision()
|
||||
);
|
||||
|
||||
System.out.println("[StationMap] renderGridIfViewportKnown zoom=" + viewportZoom
|
||||
+ " precision=" + renderPlan.precision().locatorLength()
|
||||
+ " labelStride=" + renderPlan.labelColumnStride() + "x" + renderPlan.labelRowStride()
|
||||
+ " cellPx=" + String.format(Locale.US, "%.1f/%.1f", renderPlan.estimatedCellWidthPx(), renderPlan.estimatedCellHeightPx())
|
||||
+ " cells=" + visibleGridCells.size());
|
||||
if (MAP_DEBUG_LOGGING) {
|
||||
System.out.println("[StationMap] renderGridIfViewportKnown zoom=" + viewportZoom
|
||||
+ " precision=" + renderPlan.precision().locatorLength()
|
||||
+ " labelStride=" + renderPlan.labelColumnStride() + "x" + renderPlan.labelRowStride()
|
||||
+ " cellPx=" + String.format(Locale.US, "%.1f/%.1f", renderPlan.estimatedCellWidthPx(), renderPlan.estimatedCellHeightPx())
|
||||
+ " cells=" + visibleGridCells.size());
|
||||
}
|
||||
|
||||
String gridJson = toGridJson(visibleGridCells, renderPlan);
|
||||
final String gridJson = toGridJson(visibleGridCells, renderPlan);
|
||||
if (gridJson.equals(lastRenderedGridJson)) {
|
||||
return;
|
||||
}
|
||||
@@ -1393,8 +1453,9 @@ public final class StationMapView {
|
||||
try {
|
||||
webEngine.executeScript(script);
|
||||
} catch (Exception exception) {
|
||||
System.err.println("[StationMap] executeScript failed: " + exception.getMessage());
|
||||
exception.printStackTrace();
|
||||
LOGGER.log(Level.WARNING,
|
||||
"Station map JavaScript execution failed",
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1596,17 +1657,19 @@ public final class StationMapView {
|
||||
public final class JavaMapBridge {
|
||||
|
||||
public void onMapReady() {
|
||||
System.out.println("[StationMap JS] onMapReady");
|
||||
if (MAP_DEBUG_LOGGING) {
|
||||
System.out.println("[StationMap JS] onMapReady");
|
||||
}
|
||||
mapReady = true;
|
||||
renderAll();
|
||||
requestMapInvalidateSize();
|
||||
}
|
||||
|
||||
public void onViewportChanged(double southLat,
|
||||
double westLon,
|
||||
double northLat,
|
||||
double eastLon,
|
||||
double zoom) {
|
||||
public void onViewportChanged(final double southLat,
|
||||
final double westLon,
|
||||
final double northLat,
|
||||
final double eastLon,
|
||||
final double zoom) {
|
||||
|
||||
viewportSouthLat = southLat;
|
||||
viewportWestLon = westLon;
|
||||
@@ -1614,7 +1677,9 @@ public final class StationMapView {
|
||||
viewportEastLon = eastLon;
|
||||
viewportZoom = (int) Math.round(zoom);
|
||||
|
||||
System.out.println("[StationMap JS] onViewportChanged zoom=" + viewportZoom);
|
||||
if (MAP_DEBUG_LOGGING) {
|
||||
System.out.println("[StationMap JS] onViewportChanged zoom=" + viewportZoom);
|
||||
}
|
||||
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
renderGridIfViewportKnown();
|
||||
@@ -1623,19 +1688,25 @@ public final class StationMapView {
|
||||
}
|
||||
}
|
||||
|
||||
public void onCallsignRawClicked(String callSignRaw) {
|
||||
System.out.println("[StationMap JS] onCallsignRawClicked " + callSignRaw);
|
||||
public void onCallsignRawClicked(final String callSignRaw) {
|
||||
if (MAP_DEBUG_LOGGING) {
|
||||
System.out.println("[StationMap JS] onCallsignRawClicked " + callSignRaw);
|
||||
}
|
||||
if (onCallsignRawSelected != null) {
|
||||
onCallsignRawSelected.accept(callSignRaw);
|
||||
}
|
||||
}
|
||||
|
||||
public void onJsLog(String message) {
|
||||
System.out.println("[StationMap JS] " + message);
|
||||
public void onJsLog(final String message) {
|
||||
if (MAP_DEBUG_LOGGING) {
|
||||
System.out.println("[StationMap JS] " + message);
|
||||
}
|
||||
}
|
||||
|
||||
public void onJsError(String message) {
|
||||
System.err.println("[StationMap JS ERROR] " + message);
|
||||
public void onJsError(final String message) {
|
||||
LOGGER.log(Level.WARNING,
|
||||
"Station map JavaScript error: {0}",
|
||||
message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.ChatMessage;
|
||||
|
||||
class MessageBusRawRxLoggingTest {
|
||||
|
||||
@Test
|
||||
@Timeout(5)
|
||||
void rawInboundFrameIsNotDuplicatedInDiagnosticLogging()
|
||||
throws InterruptedException {
|
||||
Logger logger = Logger.getLogger(
|
||||
MessageBusManagementThread.class.getName());
|
||||
Level previousLevel = logger.getLevel();
|
||||
boolean previousUseParentHandlers = logger.getUseParentHandlers();
|
||||
RecordingHandler handler = new RecordingHandler();
|
||||
|
||||
logger.setLevel(Level.FINE);
|
||||
logger.setUseParentHandlers(false);
|
||||
logger.addHandler(handler);
|
||||
|
||||
try {
|
||||
LinkedBlockingQueue<ChatMessage> queue =
|
||||
new LinkedBlockingQueue<>();
|
||||
MessageBusManagementThread thread =
|
||||
new MessageBusManagementThread(
|
||||
mock(ChatController.class),
|
||||
mock(ThreadStatusCallback.class),
|
||||
41L,
|
||||
queue,
|
||||
ignored -> true);
|
||||
|
||||
queue.add(message("ZZ|sensitive raw payload|"));
|
||||
queue.add(poisonPill());
|
||||
|
||||
thread.start();
|
||||
thread.join();
|
||||
|
||||
assertFalse(handler.messages().stream()
|
||||
.anyMatch(message -> message.startsWith("ON4KST RX:")));
|
||||
} finally {
|
||||
logger.removeHandler(handler);
|
||||
logger.setUseParentHandlers(previousUseParentHandlers);
|
||||
logger.setLevel(previousLevel);
|
||||
}
|
||||
}
|
||||
|
||||
private ChatMessage message(final String text) {
|
||||
ChatMessage message = new ChatMessage();
|
||||
message.setMessageText(text);
|
||||
return message;
|
||||
}
|
||||
|
||||
private ChatMessage poisonPill() {
|
||||
ChatMessage message = message(
|
||||
ApplicationConstants.DISCONNECT_RDR_POISONPILL);
|
||||
message.setMessageSenderName(
|
||||
ApplicationConstants.DISCONNECT_RDR_POISONPILL);
|
||||
return message;
|
||||
}
|
||||
|
||||
private static final class RecordingHandler extends Handler {
|
||||
private final List<String> messages = new ArrayList<>();
|
||||
|
||||
private RecordingHandler() {
|
||||
setLevel(Level.ALL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish(final LogRecord record) {
|
||||
messages.add(record.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
// No buffered output.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// No external resource.
|
||||
}
|
||||
|
||||
private List<String> messages() {
|
||||
return List.copyOf(messages);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package kst4contest.locatorUtils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class DirectionUtilsConsoleOutputTest {
|
||||
|
||||
@Test
|
||||
void outOfRangeCheckDoesNotWriteToStandardOutput() {
|
||||
PrintStream originalOutput = System.out;
|
||||
ByteArrayOutputStream capturedOutput = new ByteArrayOutputStream();
|
||||
|
||||
try (PrintStream testOutput = new PrintStream(
|
||||
capturedOutput,
|
||||
true,
|
||||
StandardCharsets.UTF_8)) {
|
||||
System.setOut(testOutput);
|
||||
|
||||
assertFalse(DirectionUtils.isInAngleAndRange(
|
||||
"JN49FL",
|
||||
"JO43XM",
|
||||
"JO30SA",
|
||||
1.0,
|
||||
50.0));
|
||||
} finally {
|
||||
System.setOut(originalOutput);
|
||||
}
|
||||
|
||||
assertEquals("", capturedOutput.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,66 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class MapHtmlResourcesContractTest {
|
||||
|
||||
@Test
|
||||
void moveEndUpdatesViewportWithoutRebuildingStationMarkers() {
|
||||
String html = MapHtmlResources.createStationMapHtml(12345);
|
||||
|
||||
String handler = eventHandler(html, "moveend");
|
||||
|
||||
assertTrue(handler.contains("notifyViewport();"));
|
||||
assertFalse(handler.contains("renderStationMarkers();"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void zoomEndStillRebuildsStationMarkersBeforeUpdatingViewport() {
|
||||
String html = MapHtmlResources.createStationMapHtml(12345);
|
||||
|
||||
String handler = eventHandler(html, "zoomend");
|
||||
int markerRender = handler.indexOf("renderStationMarkers();");
|
||||
int viewportNotification = handler.indexOf("notifyViewport();");
|
||||
|
||||
assertTrue(markerRender >= 0);
|
||||
assertTrue(viewportNotification > markerRender);
|
||||
}
|
||||
|
||||
@Test
|
||||
void stationDataChangesStillRebuildStationMarkers() {
|
||||
String html = MapHtmlResources.createStationMapHtml(12345);
|
||||
|
||||
int setterStart = html.indexOf("function setStations(stationsJson)");
|
||||
int nextFunction = html.indexOf("function setBeam(beamJson)", setterStart);
|
||||
String setterBody = html.substring(setterStart, nextFunction);
|
||||
|
||||
assertTrue(setterBody.contains("stationData = JSON.parse(stationsJson);"));
|
||||
assertTrue(setterBody.contains("renderStationMarkers();"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void viewportNotificationForwardsBoundsAndZoomToJava() {
|
||||
String html = MapHtmlResources.createStationMapHtml(12345);
|
||||
|
||||
String functionBody = functionBody(html, "function notifyViewport()", "function gridLineColor()");
|
||||
|
||||
assertTrue(functionBody.contains("const bounds = map.getBounds();"));
|
||||
assertTrue(functionBody.contains("const zoom = map.getZoom();"));
|
||||
assertTrue(functionBody.contains("window.javaMapBridge.onViewportChanged("));
|
||||
assertTrue(functionBody.contains("bounds.getSouth(),"));
|
||||
assertTrue(functionBody.contains("bounds.getWest(),"));
|
||||
assertTrue(functionBody.contains("bounds.getNorth(),"));
|
||||
assertTrue(functionBody.contains("bounds.getEast(),"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stationHitTestingKeepsRawCallsignSelectionContract() {
|
||||
String html = MapHtmlResources.createStationMapHtml(12345);
|
||||
|
||||
String functionBody = functionBody(html, "function inspectPoint(x, y)", "function setHome(lat, lon, zoom)");
|
||||
|
||||
assertTrue(functionBody.contains("el.closest('.station-marker-root')"));
|
||||
assertTrue(functionBody.contains("stationRoot.getAttribute('data-callsignraw')"));
|
||||
assertTrue(functionBody.contains("return 'station|' + callSignRaw"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stationClusteringCanBeToggledWithoutReplacingStationData() {
|
||||
String html = MapHtmlResources.createStationMapHtml(12345);
|
||||
@@ -28,4 +88,23 @@ class MapHtmlResourcesContractTest {
|
||||
assertFalse(setterBody.contains("stationData ="));
|
||||
assertTrue(html.contains("setStationClusteringEnabled: setStationClusteringEnabled"));
|
||||
}
|
||||
|
||||
private static String eventHandler(String html, String eventName) {
|
||||
String signature = "map.on('" + eventName + "', function () {";
|
||||
int handlerStart = html.indexOf(signature);
|
||||
int handlerEnd = html.indexOf("});", handlerStart);
|
||||
|
||||
assertTrue(handlerStart >= 0);
|
||||
assertTrue(handlerEnd > handlerStart);
|
||||
return html.substring(handlerStart, handlerEnd);
|
||||
}
|
||||
|
||||
private static String functionBody(String html, String signature, String nextSignature) {
|
||||
int functionStart = html.indexOf(signature);
|
||||
int functionEnd = html.indexOf(nextSignature, functionStart);
|
||||
|
||||
assertTrue(functionStart >= 0);
|
||||
assertTrue(functionEnd > functionStart);
|
||||
return html.substring(functionStart, functionEnd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package kst4contest.view.map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class StationMapPerformanceSupportTest {
|
||||
|
||||
@Test
|
||||
void hiddenRefreshIsDeferredUntilTheNextShowOnly() {
|
||||
StationMapPerformanceSupport.RefreshState state =
|
||||
new StationMapPerformanceSupport.RefreshState();
|
||||
|
||||
assertFalse(state.shouldRefreshNow(false));
|
||||
assertTrue(state.consumeDirtyOnShow());
|
||||
assertFalse(state.consumeDirtyOnShow());
|
||||
}
|
||||
|
||||
@Test
|
||||
void visibleRefreshCanRunImmediately() {
|
||||
StationMapPerformanceSupport.RefreshState state =
|
||||
new StationMapPerformanceSupport.RefreshState();
|
||||
|
||||
assertTrue(state.shouldRefreshNow(true));
|
||||
assertFalse(state.consumeDirtyOnShow());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unchangedGridInputsAreRejectedBeforeRendering() {
|
||||
StationMapPerformanceSupport.GridRenderState state =
|
||||
new StationMapPerformanceSupport.GridRenderState();
|
||||
StationMapPerformanceSupport.GridRenderInput input = gridInput();
|
||||
|
||||
assertTrue(state.shouldRender(input));
|
||||
assertFalse(state.shouldRender(input));
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyGridInputCanTriggerANewRender() {
|
||||
assertChanged(new StationMapPerformanceSupport.GridRenderInput(
|
||||
49.1, 6.0, 55.0, 16.0, 7, 1024.0, 768.0));
|
||||
assertChanged(new StationMapPerformanceSupport.GridRenderInput(
|
||||
49.0, 6.1, 55.0, 16.0, 7, 1024.0, 768.0));
|
||||
assertChanged(new StationMapPerformanceSupport.GridRenderInput(
|
||||
49.0, 6.0, 55.1, 16.0, 7, 1024.0, 768.0));
|
||||
assertChanged(new StationMapPerformanceSupport.GridRenderInput(
|
||||
49.0, 6.0, 55.0, 16.1, 7, 1024.0, 768.0));
|
||||
assertChanged(new StationMapPerformanceSupport.GridRenderInput(
|
||||
49.0, 6.0, 55.0, 16.0, 8, 1024.0, 768.0));
|
||||
assertChanged(new StationMapPerformanceSupport.GridRenderInput(
|
||||
49.0, 6.0, 55.0, 16.0, 7, 1200.0, 768.0));
|
||||
assertChanged(new StationMapPerformanceSupport.GridRenderInput(
|
||||
49.0, 6.0, 55.0, 16.0, 7, 1024.0, 800.0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void disabledMapDebugSkipsTheDiagnosticOperation() {
|
||||
AtomicBoolean executed = new AtomicBoolean();
|
||||
|
||||
StationMapPerformanceSupport.runDebugIfEnabled(
|
||||
false,
|
||||
() -> executed.set(true));
|
||||
|
||||
assertFalse(executed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void enabledMapDebugRunsTheDiagnosticOperation() {
|
||||
AtomicBoolean executed = new AtomicBoolean();
|
||||
|
||||
StationMapPerformanceSupport.runDebugIfEnabled(
|
||||
true,
|
||||
() -> executed.set(true));
|
||||
|
||||
assertTrue(executed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void slowRefreshStartsAtOneHundredMilliseconds() {
|
||||
assertFalse(StationMapPerformanceSupport.isSlowRefresh(99_999_999L));
|
||||
assertTrue(StationMapPerformanceSupport.isSlowRefresh(100_000_000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void slowRefreshMessageSeparatesSnapshotAndRenderTimes() {
|
||||
assertEquals(
|
||||
"Station map refresh slow: total=245 ms, snapshots=42 ms, "
|
||||
+ "render=198 ms, visibleMembers=500, stations=487",
|
||||
StationMapPerformanceSupport.formatSlowRefreshMessage(
|
||||
245_000_000L,
|
||||
42_000_000L,
|
||||
198_000_000L,
|
||||
500,
|
||||
487
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void assertChanged(
|
||||
StationMapPerformanceSupport.GridRenderInput changed) {
|
||||
StationMapPerformanceSupport.GridRenderState state =
|
||||
new StationMapPerformanceSupport.GridRenderState();
|
||||
assertTrue(state.shouldRender(gridInput()));
|
||||
assertTrue(state.shouldRender(changed));
|
||||
}
|
||||
|
||||
private StationMapPerformanceSupport.GridRenderInput gridInput() {
|
||||
return new StationMapPerformanceSupport.GridRenderInput(
|
||||
49.0,
|
||||
6.0,
|
||||
55.0,
|
||||
16.0,
|
||||
7,
|
||||
1024.0,
|
||||
768.0
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user