mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-19 05:42:58 +02:00
Optimize station map refresh performance
This commit is contained in:
1 parent
df70cb1943
commit
3e8457eede
12 files changed
+626
-82
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.
|
- 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.
|
- **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.
|
- 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.
|
- 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.
|
- 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();
|
// e.printStackTrace();
|
||||||
// }
|
// }
|
||||||
|
|
||||||
if (!On4KstProtocol.isInternalDxqResponse(
|
|
||||||
messageTextRaw.getMessageText())) {
|
|
||||||
LOGGER.log(Level.FINE, "ON4KST RX: {0}",
|
|
||||||
messageTextRaw.getMessageText());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
processRXMessage23001(messageTextRaw);
|
processRXMessage23001(messageTextRaw);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ public class Utils4KST {
|
|||||||
public String time_generateCurrenthhmmZTimeStringForClusterMessage() {
|
public String time_generateCurrenthhmmZTimeStringForClusterMessage() {
|
||||||
|
|
||||||
OffsetDateTime currentTimeInUtc = OffsetDateTime.now(ZoneOffset.UTC);
|
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";
|
return currentTimeInUtc.format(DateTimeFormatter.ofPattern("HHmm"))+"Z";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ public class DirectionUtils {
|
|||||||
|
|
||||||
// Check if distance exceeds my setted maximum range
|
// Check if distance exceeds my setted maximum range
|
||||||
if (distanceFromMeToLocSender > maxRangeKm) {
|
if (distanceFromMeToLocSender > maxRangeKm) {
|
||||||
System.out.println("too far, " + distanceFromMeToLocSender + " km");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -795,8 +795,8 @@ public final class MapHtmlResources {
|
|||||||
/**
|
/**
|
||||||
* Re-renders station markers for the current zoom and viewport.
|
* Re-renders station markers for the current zoom and viewport.
|
||||||
*
|
*
|
||||||
* This is called when stations arrive from Java and after zoom/move
|
* This is called when stations arrive from Java, clustering changes and
|
||||||
* events, because screen-grid clusters depend on the current viewport.
|
* after zoom events. Panning keeps the existing screen-grid clusters.
|
||||||
*/
|
*/
|
||||||
function renderStationMarkers() {
|
function renderStationMarkers() {
|
||||||
if (!map || !stationLayer) {
|
if (!map || !stationLayer) {
|
||||||
@@ -941,7 +941,6 @@ public final class MapHtmlResources {
|
|||||||
});
|
});
|
||||||
|
|
||||||
map.on('moveend', function () {
|
map.on('moveend', function () {
|
||||||
renderStationMarkers();
|
|
||||||
notifyViewport();
|
notifyViewport();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import java.util.Objects;
|
|||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import kst4contest.model.Band;
|
import kst4contest.model.Band;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
@@ -34,6 +36,9 @@ import java.util.function.Predicate;
|
|||||||
*/
|
*/
|
||||||
public final class StationMapBridge {
|
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);
|
private final AtomicLong pathAnalysisGeneration = new AtomicLong(0);
|
||||||
|
|
||||||
@@ -48,6 +53,9 @@ public final class StationMapBridge {
|
|||||||
|
|
||||||
|
|
||||||
private final MapCallsignRawSnapshotBuilder snapshotBuilder = new MapCallsignRawSnapshotBuilder();
|
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 = "";
|
private String lastPathAnalysisRequestSignature = "";
|
||||||
@@ -131,10 +139,14 @@ public final class StationMapBridge {
|
|||||||
|
|
||||||
public void showWindow() {
|
public void showWindow() {
|
||||||
stationMapView.showWindow();
|
stationMapView.showWindow();
|
||||||
requestImmediateRefresh();
|
if (refreshState.consumeDirtyOnShow()) {
|
||||||
|
requestImmediateRefresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void hideWindow() {
|
public void hideWindow() {
|
||||||
|
refreshCoalescer.stop();
|
||||||
|
refreshState.markDirty();
|
||||||
stationMapView.hideWindow();
|
stationMapView.hideWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,19 +196,35 @@ public final class StationMapBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleRefresh() {
|
private void scheduleRefresh() {
|
||||||
if (Platform.isFxApplicationThread()) {
|
final Runnable scheduleAction = () -> {
|
||||||
|
if (!stationMapView.isShowing()) {
|
||||||
|
refreshCoalescer.stop();
|
||||||
|
refreshState.markDirty();
|
||||||
|
return;
|
||||||
|
}
|
||||||
refreshCoalescer.playFromStart();
|
refreshCoalescer.playFromStart();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Platform.isFxApplicationThread()) {
|
||||||
|
scheduleAction.run();
|
||||||
} else {
|
} else {
|
||||||
Platform.runLater(() -> refreshCoalescer.playFromStart());
|
Platform.runLater(scheduleAction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshNow() {
|
private void refreshNow() {
|
||||||
List<ChatMember> visibleChatMembers = new ArrayList<>(chatController.getLst_chatMemberSortedFilteredList());
|
if (!refreshState.shouldRefreshNow(stationMapView.isShowing())) {
|
||||||
ChatMember selectedChatMember = chatController.getScoreService().getSelectedChatMember();
|
refreshCoalescer.stop();
|
||||||
EnumSet<Band> selectedBands = chatController.getReachabilityService().getEnabledStationBands();
|
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,
|
visibleChatMembers,
|
||||||
selectedChatMember,
|
selectedChatMember,
|
||||||
selectedBands
|
selectedBands
|
||||||
@@ -204,17 +232,19 @@ public final class StationMapBridge {
|
|||||||
|
|
||||||
MapCallsignRawSnapshot selectedSnapshot = null;
|
MapCallsignRawSnapshot selectedSnapshot = null;
|
||||||
if (selectedChatMember != null && selectedChatMember.getCallSignRaw() != null) {
|
if (selectedChatMember != null && selectedChatMember.getCallSignRaw() != null) {
|
||||||
String selectedCallsignRaw = normalizeCallsignRaw(selectedChatMember.getCallSignRaw());
|
final String selectedCallsignRaw = normalizeCallsignRaw(selectedChatMember.getCallSignRaw());
|
||||||
selectedSnapshot = snapshots.stream()
|
selectedSnapshot = snapshots.stream()
|
||||||
.filter(snapshot -> snapshot.callSignRaw().equals(selectedCallsignRaw))
|
.filter(snapshot -> snapshot.callSignRaw().equals(selectedCallsignRaw))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.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(
|
stationMapView.refreshMap(
|
||||||
snapshots,
|
snapshots,
|
||||||
selectedSnapshot,
|
selectedSnapshot,
|
||||||
@@ -224,8 +254,22 @@ public final class StationMapBridge {
|
|||||||
preferences.getStn_maxQRBDefault(),
|
preferences.getStn_maxQRBDefault(),
|
||||||
filteredViewActive
|
filteredViewActive
|
||||||
);
|
);
|
||||||
|
final long renderNanos = System.nanoTime() - renderStart;
|
||||||
|
|
||||||
requestPathAnalysisAsync(preferences.getStn_loginLocatorMainCat(), selectedSnapshot);
|
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) {
|
private void handleMapCallsignSelection(String callSignRaw) {
|
||||||
System.out.println("########################### map selected callsign " + callSignRaw);
|
|
||||||
|
|
||||||
ChatMember resolved = resolveBestChatMember(callSignRaw);
|
ChatMember resolved = resolveBestChatMember(callSignRaw);
|
||||||
if (resolved == null) {
|
if (resolved == null) {
|
||||||
return;
|
return;
|
||||||
@@ -436,4 +478,4 @@ public final class StationMapBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
@@ -44,6 +46,10 @@ import javafx.scene.layout.HBox;
|
|||||||
*/
|
*/
|
||||||
public final class StationMapView {
|
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.
|
* Enables verbose map debug output.
|
||||||
*
|
*
|
||||||
@@ -130,6 +136,9 @@ public final class StationMapView {
|
|||||||
private String lastRenderedBeamJson = "";
|
private String lastRenderedBeamJson = "";
|
||||||
private String lastRenderedConnectionJson = "";
|
private String lastRenderedConnectionJson = "";
|
||||||
private String lastRenderedGridJson = "";
|
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.
|
* Prevent periodic refreshes from panning the map back to the selected station.
|
||||||
@@ -195,7 +204,8 @@ public final class StationMapView {
|
|||||||
try {
|
try {
|
||||||
tileProxyServer = new TileProxyServer();
|
tileProxyServer = new TileProxyServer();
|
||||||
} catch (IOException e) {
|
} 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();
|
initializeUi();
|
||||||
initializeWebView();
|
initializeWebView();
|
||||||
@@ -362,8 +372,14 @@ public final class StationMapView {
|
|||||||
webView.setFocusTraversable(true);
|
webView.setFocusTraversable(true);
|
||||||
webView.setPickOnBounds(true);
|
webView.setPickOnBounds(true);
|
||||||
|
|
||||||
webView.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> logWebViewMouseEvent("MOUSE_PRESSED", event));
|
if (MAP_DEBUG_LOGGING) {
|
||||||
webView.addEventHandler(MouseEvent.MOUSE_RELEASED, event -> logWebViewMouseEvent("MOUSE_RELEASED", event));
|
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(MouseEvent.MOUSE_CLICKED, event -> handleWebViewClick(event));
|
||||||
|
|
||||||
webView.addEventHandler(ScrollEvent.SCROLL, event -> {
|
webView.addEventHandler(ScrollEvent.SCROLL, event -> {
|
||||||
@@ -374,8 +390,10 @@ public final class StationMapView {
|
|||||||
+ " deltaY=" + event.getDeltaY());
|
+ " deltaY=" + event.getDeltaY());
|
||||||
}
|
}
|
||||||
|
|
||||||
InteractiveTarget target = inspectInteractiveTarget(event.getX(), event.getY());
|
|
||||||
if (MAP_DEBUG_LOGGING) {
|
if (MAP_DEBUG_LOGGING) {
|
||||||
|
final InteractiveTarget target = inspectInteractiveTarget(
|
||||||
|
event.getX(),
|
||||||
|
event.getY());
|
||||||
System.out.println("[StationMap FX] inspect scroll -> " + target);
|
System.out.println("[StationMap FX] inspect scroll -> " + target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,19 +802,21 @@ public final class StationMapView {
|
|||||||
try {
|
try {
|
||||||
Object result = webEngine.executeScript("window.kstMapApi.getViewportState();");
|
Object result = webEngine.executeScript("window.kstMapApi.getViewportState();");
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
System.err.println("[StationMap FX] getViewportState returned null");
|
LOGGER.warning("Station map getViewportState returned null");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String raw = result.toString();
|
String raw = result.toString();
|
||||||
if (raw.isBlank()) {
|
if (raw.isBlank()) {
|
||||||
System.err.println("[StationMap FX] getViewportState returned blank");
|
LOGGER.warning("Station map getViewportState returned blank");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] parts = raw.split("\\|");
|
String[] parts = raw.split("\\|");
|
||||||
if (parts.length != 5) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,15 +826,19 @@ public final class StationMapView {
|
|||||||
viewportEastLon = Double.parseDouble(parts[3]);
|
viewportEastLon = Double.parseDouble(parts[3]);
|
||||||
viewportZoom = (int) Math.round(Double.parseDouble(parts[4]));
|
viewportZoom = (int) Math.round(Double.parseDouble(parts[4]));
|
||||||
|
|
||||||
System.out.println("[StationMap FX] pulled viewport south=" + viewportSouthLat
|
if (MAP_DEBUG_LOGGING) {
|
||||||
+ " west=" + viewportWestLon
|
System.out.println("[StationMap FX] pulled viewport south=" + viewportSouthLat
|
||||||
+ " north=" + viewportNorthLat
|
+ " west=" + viewportWestLon
|
||||||
+ " east=" + viewportEastLon
|
+ " north=" + viewportNorthLat
|
||||||
+ " zoom=" + viewportZoom);
|
+ " east=" + viewportEastLon
|
||||||
|
+ " zoom=" + viewportZoom);
|
||||||
|
}
|
||||||
|
|
||||||
renderGridIfViewportKnown();
|
renderGridIfViewportKnown();
|
||||||
} catch (Exception exception) {
|
} 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);
|
logWebViewMouseEvent("MOUSE_CLICKED", event);
|
||||||
|
|
||||||
InteractiveTarget target = inspectInteractiveTarget(event.getX(), event.getY());
|
final InteractiveTarget target = inspectInteractiveTarget(
|
||||||
System.out.println("[StationMap FX] inspect click -> " + target);
|
event.getX(), event.getY());
|
||||||
|
if (MAP_DEBUG_LOGGING) {
|
||||||
|
System.out.println("[StationMap FX] inspect click -> " + target);
|
||||||
|
}
|
||||||
|
|
||||||
switch (target.kind()) {
|
switch (target.kind()) {
|
||||||
case STATION -> {
|
case STATION -> {
|
||||||
if (target.callSignRaw() != null && !target.callSignRaw().isBlank() && onCallsignRawSelected != null) {
|
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());
|
onCallsignRawSelected.accept(target.callSignRaw());
|
||||||
event.consume();
|
event.consume();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case ZOOM_IN -> {
|
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();");
|
executeMapScriptSafely("window.kstMapApi.zoomIn();");
|
||||||
requestViewportPullFromJs();
|
requestViewportPullFromJs();
|
||||||
event.consume();
|
event.consume();
|
||||||
}
|
}
|
||||||
case ZOOM_OUT -> {
|
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();");
|
executeMapScriptSafely("window.kstMapApi.zoomOut();");
|
||||||
requestViewportPullFromJs();
|
requestViewportPullFromJs();
|
||||||
event.consume();
|
event.consume();
|
||||||
@@ -887,18 +920,27 @@ public final class StationMapView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void logWebViewMouseEvent(String type, MouseEvent event) {
|
private void logWebViewMouseEvent(
|
||||||
System.out.println("[StationMap FX] " + type
|
final String type,
|
||||||
+ " x=" + (int) event.getX()
|
final MouseEvent event
|
||||||
+ " y=" + (int) event.getY()
|
) {
|
||||||
+ " target=" + event.getTarget().getClass().getSimpleName()
|
StationMapPerformanceSupport.runDebugIfEnabled(
|
||||||
+ " button=" + event.getButton());
|
MAP_DEBUG_LOGGING,
|
||||||
|
() -> {
|
||||||
probeDomElementAt(event.getX(), event.getY());
|
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) {
|
private void probeDomElementAt(
|
||||||
if (!mapReady) {
|
final double xCoordinate,
|
||||||
|
final double yCoordinate
|
||||||
|
) {
|
||||||
|
if (!MAP_DEBUG_LOGGING || !mapReady) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -921,13 +963,15 @@ public final class StationMapView {
|
|||||||
}
|
}
|
||||||
return "tag=" + el.tagName + " class=" + cls + " id=" + id + " text=" + text;
|
return "tag=" + el.tagName + " class=" + cls + " id=" + id + " text=" + text;
|
||||||
})();
|
})();
|
||||||
""", x, y);
|
""", xCoordinate, yCoordinate);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Object result = webEngine.executeScript(script);
|
Object result = webEngine.executeScript(script);
|
||||||
System.out.println("[StationMap FX] elementFromPoint -> " + result);
|
System.out.println("[StationMap FX] elementFromPoint -> " + result);
|
||||||
} catch (Exception exception) {
|
} 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);
|
default -> new InteractiveTarget(InteractiveKind.NONE, "", tag, cssClass, text);
|
||||||
};
|
};
|
||||||
} catch (Exception exception) {
|
} 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();
|
return InteractiveTarget.none();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1185,7 +1231,6 @@ public final class StationMapView {
|
|||||||
renderStations();
|
renderStations();
|
||||||
renderBeam();
|
renderBeam();
|
||||||
renderConnectionLine();
|
renderConnectionLine();
|
||||||
renderGridIfViewportKnown();
|
|
||||||
focusSelectedStationOnlyWhenSelectionChanged();
|
focusSelectedStationOnlyWhenSelectionChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1269,10 +1314,23 @@ public final class StationMapView {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
double viewportWidthPx = Math.max(1.0, webView.getWidth());
|
final double viewportWidthPx = Math.max(1.0, webView.getWidth());
|
||||||
double viewportHeightPx = Math.max(1.0, webView.getHeight());
|
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,
|
viewportZoom,
|
||||||
viewportSouthLat,
|
viewportSouthLat,
|
||||||
viewportWestLon,
|
viewportWestLon,
|
||||||
@@ -1282,7 +1340,7 @@ public final class StationMapView {
|
|||||||
viewportHeightPx
|
viewportHeightPx
|
||||||
);
|
);
|
||||||
|
|
||||||
List<MaidenheadGridUtils.GridCell> visibleGridCells = MaidenheadGridUtils.buildVisibleCells(
|
final List<MaidenheadGridUtils.GridCell> visibleGridCells = MaidenheadGridUtils.buildVisibleCells(
|
||||||
viewportSouthLat,
|
viewportSouthLat,
|
||||||
viewportWestLon,
|
viewportWestLon,
|
||||||
viewportNorthLat,
|
viewportNorthLat,
|
||||||
@@ -1290,13 +1348,15 @@ public final class StationMapView {
|
|||||||
renderPlan.precision()
|
renderPlan.precision()
|
||||||
);
|
);
|
||||||
|
|
||||||
System.out.println("[StationMap] renderGridIfViewportKnown zoom=" + viewportZoom
|
if (MAP_DEBUG_LOGGING) {
|
||||||
+ " precision=" + renderPlan.precision().locatorLength()
|
System.out.println("[StationMap] renderGridIfViewportKnown zoom=" + viewportZoom
|
||||||
+ " labelStride=" + renderPlan.labelColumnStride() + "x" + renderPlan.labelRowStride()
|
+ " precision=" + renderPlan.precision().locatorLength()
|
||||||
+ " cellPx=" + String.format(Locale.US, "%.1f/%.1f", renderPlan.estimatedCellWidthPx(), renderPlan.estimatedCellHeightPx())
|
+ " labelStride=" + renderPlan.labelColumnStride() + "x" + renderPlan.labelRowStride()
|
||||||
+ " cells=" + visibleGridCells.size());
|
+ " 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)) {
|
if (gridJson.equals(lastRenderedGridJson)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1393,8 +1453,9 @@ public final class StationMapView {
|
|||||||
try {
|
try {
|
||||||
webEngine.executeScript(script);
|
webEngine.executeScript(script);
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
System.err.println("[StationMap] executeScript failed: " + exception.getMessage());
|
LOGGER.log(Level.WARNING,
|
||||||
exception.printStackTrace();
|
"Station map JavaScript execution failed",
|
||||||
|
exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1596,17 +1657,19 @@ public final class StationMapView {
|
|||||||
public final class JavaMapBridge {
|
public final class JavaMapBridge {
|
||||||
|
|
||||||
public void onMapReady() {
|
public void onMapReady() {
|
||||||
System.out.println("[StationMap JS] onMapReady");
|
if (MAP_DEBUG_LOGGING) {
|
||||||
|
System.out.println("[StationMap JS] onMapReady");
|
||||||
|
}
|
||||||
mapReady = true;
|
mapReady = true;
|
||||||
renderAll();
|
renderAll();
|
||||||
requestMapInvalidateSize();
|
requestMapInvalidateSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onViewportChanged(double southLat,
|
public void onViewportChanged(final double southLat,
|
||||||
double westLon,
|
final double westLon,
|
||||||
double northLat,
|
final double northLat,
|
||||||
double eastLon,
|
final double eastLon,
|
||||||
double zoom) {
|
final double zoom) {
|
||||||
|
|
||||||
viewportSouthLat = southLat;
|
viewportSouthLat = southLat;
|
||||||
viewportWestLon = westLon;
|
viewportWestLon = westLon;
|
||||||
@@ -1614,7 +1677,9 @@ public final class StationMapView {
|
|||||||
viewportEastLon = eastLon;
|
viewportEastLon = eastLon;
|
||||||
viewportZoom = (int) Math.round(zoom);
|
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()) {
|
if (Platform.isFxApplicationThread()) {
|
||||||
renderGridIfViewportKnown();
|
renderGridIfViewportKnown();
|
||||||
@@ -1623,19 +1688,25 @@ public final class StationMapView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onCallsignRawClicked(String callSignRaw) {
|
public void onCallsignRawClicked(final String callSignRaw) {
|
||||||
System.out.println("[StationMap JS] onCallsignRawClicked " + callSignRaw);
|
if (MAP_DEBUG_LOGGING) {
|
||||||
|
System.out.println("[StationMap JS] onCallsignRawClicked " + callSignRaw);
|
||||||
|
}
|
||||||
if (onCallsignRawSelected != null) {
|
if (onCallsignRawSelected != null) {
|
||||||
onCallsignRawSelected.accept(callSignRaw);
|
onCallsignRawSelected.accept(callSignRaw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onJsLog(String message) {
|
public void onJsLog(final String message) {
|
||||||
System.out.println("[StationMap JS] " + message);
|
if (MAP_DEBUG_LOGGING) {
|
||||||
|
System.out.println("[StationMap JS] " + message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onJsError(String message) {
|
public void onJsError(final String message) {
|
||||||
System.err.println("[StationMap JS ERROR] " + 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 {
|
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
|
@Test
|
||||||
void stationClusteringCanBeToggledWithoutReplacingStationData() {
|
void stationClusteringCanBeToggledWithoutReplacingStationData() {
|
||||||
String html = MapHtmlResources.createStationMapHtml(12345);
|
String html = MapHtmlResources.createStationMapHtml(12345);
|
||||||
@@ -28,4 +88,23 @@ class MapHtmlResourcesContractTest {
|
|||||||
assertFalse(setterBody.contains("stationData ="));
|
assertFalse(setterBody.contains("stationData ="));
|
||||||
assertTrue(html.contains("setStationClusteringEnabled: setStationClusteringEnabled"));
|
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