Add optional station map clustering toggle (solves #79) and added documentation

This commit is contained in:
Marc Froehlich
2026-09-03 01:26:19 +02:00
parent 595fb84362
commit 53555cbe69
16 changed files with 250 additions and 16 deletions
@@ -55,7 +55,7 @@ public class ChatPreferences {
* Reading must stay backwards compatible: missing/unknown tags should fall back to defaults.
*/
// private static final int CONFIG_VERSION = 2;
public static final int CONFIG_VERSION = 6;
public static final int CONFIG_VERSION = 7;
// Prefer writing tag names that mirror variable names (human readable). Keep legacy tags for compatibility.
private static final String TAG_CONFIG_VERSION = "configVersion";
@@ -346,6 +346,7 @@ public class ChatPreferences {
private double[] GUIstationMapStageSceneSizeHW = new double[] { 1000, 800 };
private double[] GUIstationMapStagePositionXY = new double[] { Double.NaN, Double.NaN };
private boolean GUIstationMapPathAnalysisVisible = true;
private boolean GUIstationMapClusteringEnabled = true;
private final Map<String, Double> tableColumnWidths = new LinkedHashMap<>();
private static final String TAG_TABLE_COLUMN_WIDTH = "tableColumnWidth";
@@ -655,6 +656,14 @@ public class ChatPreferences {
this.GUIstationMapPathAnalysisVisible = GUIstationMapPathAnalysisVisible;
}
public boolean isGUIstationMapClusteringEnabled() {
return GUIstationMapClusteringEnabled;
}
public void setGUIstationMapClusteringEnabled(boolean GUIstationMapClusteringEnabled) {
this.GUIstationMapClusteringEnabled = GUIstationMapClusteringEnabled;
}
/**
* Returns a stored width for one stable table/leaf-column identity.
*
@@ -2120,6 +2129,12 @@ public class ChatPreferences {
);
guiOptions.appendChild(GUIstationMapPathAnalysisVisible);
Element GUIstationMapClusteringEnabled = doc.createElement("GUIstationMapClusteringEnabled");
GUIstationMapClusteringEnabled.setTextContent(
String.valueOf(this.isGUIstationMapClusteringEnabled())
);
guiOptions.appendChild(GUIstationMapClusteringEnabled);
appendTableColumnWidths(doc, guiOptions);
/****************************************************************************************
@@ -2204,6 +2219,8 @@ public class ChatPreferences {
getGUIstationMapStageSceneSizeHW()[0] + ";" + getGUIstationMapStageSceneSizeHW()[1]);
upsertDirectChildText(document, guiOptions, "GUIstationMapStagePositionXY",
getGUIstationMapStagePositionXY()[0] + ";" + getGUIstationMapStagePositionXY()[1]);
upsertDirectChildText(document, guiOptions, "GUIstationMapClusteringEnabled",
String.valueOf(isGUIstationMapClusteringEnabled()));
}
private void appendTableColumnWidths(Document document, Element guiOptions) {
@@ -2956,6 +2973,7 @@ public class ChatPreferences {
* case read GUI options
*
***********************************************/
this.setGUIstationMapClusteringEnabled(true);
list = doc.getElementsByTagName("guiOptions");
if (list.getLength() != 0) {
@@ -2994,6 +3012,17 @@ public class ChatPreferences {
"GUIstationMapPathAnalysisVisible"
));
/*
* Files written before config version 7 do not contain this value.
* Missing or malformed values keep clustering enabled so existing
* installations retain the established map behaviour.
*/
this.setGUIstationMapClusteringEnabled(getBooleanOrDefault(
element,
true,
"GUIstationMapClusteringEnabled"
));
// Splitpane divider positions
String s1 = getText(element, null, "GUIselectedCallSignSplitPane_dividerposition");
if (s1 != null) {
@@ -3344,6 +3373,20 @@ public class ChatPreferences {
return "true".equalsIgnoreCase(v) || "1".equals(v) || "yes".equalsIgnoreCase(v);
}
private static boolean getBooleanOrDefault(Element parent, boolean defaultValue, String... tagNames) {
String value = getText(parent, null, tagNames);
if (value == null) {
return defaultValue;
}
if ("true".equalsIgnoreCase(value) || "1".equals(value) || "yes".equalsIgnoreCase(value)) {
return true;
}
if ("false".equalsIgnoreCase(value) || "0".equals(value) || "no".equalsIgnoreCase(value)) {
return false;
}
return defaultValue;
}
private static int getInt(Element parent, int defaultValue, String... tagNames) {
String v = getText(parent, null, tagNames);
if (v == null) {
@@ -15,6 +15,7 @@ import java.nio.charset.StandardCharsets;
* - grid / beam / connection use non-interactive panes
* - JavaScript errors are forwarded to Java through javaMapBridge
* - setTheme(light|dark) aligns the map with the JavaFX application theme
* - setStationClusteringEnabled(boolean) re-renders the existing station data
*
* Important:
* This version intentionally uses integer Leaflet zoom levels again.
@@ -356,6 +357,7 @@ public final class MapHtmlResources {
*/
let stationData = [];
let stationsByCallsignRaw = {};
let stationClusteringEnabled = true;
let clustersById = {};
let clusterSequence = 0;
@@ -810,13 +812,19 @@ public final class MapHtmlResources {
return;
}
if (Number(map.getZoom()) >= KST_CLUSTER_DISABLE_ZOOM) {
if (!stationClusteringEnabled
|| Number(map.getZoom()) >= KST_CLUSTER_DISABLE_ZOOM) {
renderAllStationsIndividually();
} else {
renderClusteredStations();
}
}
function setStationClusteringEnabled(enabled) {
stationClusteringEnabled = Boolean(enabled);
renderStationMarkers();
}
/**
* Zooms into a cluster.
*
@@ -1271,6 +1279,7 @@ public final class MapHtmlResources {
getViewportState: getViewportState,
setHome: setHome,
setStations: setStations,
setStationClusteringEnabled: setStationClusteringEnabled,
setBeam: setBeam,
setConnection: setConnection,
setProfileHoverPoint: setProfileHoverPoint,
@@ -1284,4 +1293,4 @@ public final class MapHtmlResources {
</html>
""".replace("__TILE_PROXY_PORT__", String.valueOf(tileProxyPort));
}
}
}
@@ -90,6 +90,9 @@ public final class StationMapView {
private final Button resetViewButton = new Button("Reset view");
private final Tooltip statusTooltip = new Tooltip();
private final CheckBox stationClusteringCheckBox = new CheckBox("Group nearby stations");
private final Tooltip stationClusteringTooltip = new Tooltip(
"Group nearby stations into clusters at lower zoom levels.");
private Runnable onResetView;
@@ -333,6 +336,21 @@ public final class StationMapView {
}
});
stationClusteringCheckBox.setMinWidth(Region.USE_PREF_SIZE);
stationClusteringCheckBox.setTooltip(stationClusteringTooltip);
stationClusteringCheckBox.setAccessibleText("Group nearby stations");
stationClusteringCheckBox.setAccessibleHelp(stationClusteringTooltip.getText());
stationClusteringCheckBox.setSelected(chatPreferences.isGUIstationMapClusteringEnabled());
stationClusteringCheckBox.selectedProperty().addListener((obs, oldValue, newValue) -> {
boolean enabled = newValue;
chatPreferences.setGUIstationMapClusteringEnabled(enabled);
if (mapReady) {
executeMapScriptSafely(
"window.kstMapApi.setStationClusteringEnabled(" + enabled + ");");
}
layoutSaveRequester.run();
});
pathAnalysisVisibilityButton.setMinWidth(Region.USE_PREF_SIZE);
pathAnalysisVisibilityButton.setTooltip(pathAnalysisVisibilityTooltip);
pathAnalysisVisibilityButton.setOnAction(event ->
@@ -535,6 +553,7 @@ public final class StationMapView {
statusLabel,
triggerClusterSpotButton,
resetViewButton,
stationClusteringCheckBox,
pathAnalysisHiddenHintLabel,
pathAnalysisVisibilityButton
);
@@ -808,6 +827,9 @@ public final class StationMapView {
window.setMember("javaMapBridge", javaMapBridge);
executeMapScriptSafely("window.kstMapApi.init();");
executeMapScriptSafely(
"window.kstMapApi.setStationClusteringEnabled("
+ chatPreferences.isGUIstationMapClusteringEnabled() + ");");
mapReady = true;
applyMapThemeToWebView(chatPreferences.isGUI_darkModeActive());