mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-09-12 12:15:33 +02:00
Add optional station map clustering toggle (solves #79) and added documentation
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -103,6 +103,7 @@ class ChatPreferencesLayoutPersistenceTest {
|
||||
preferences.setStn_loginCallSign("UNSAVED-CALL");
|
||||
preferences.getGUIscn_ChatwindowMainSceneSizeHW()[0] = 812;
|
||||
preferences.getGUIscn_ChatwindowMainSceneSizeHW()[1] = 1340;
|
||||
preferences.setGUIstationMapClusteringEnabled(false);
|
||||
preferences.setTableColumnWidth("qso-other-monitor", "call-tx", 123.75);
|
||||
|
||||
assertTrue(preferences.writeLayoutPreferencesToXmlFile());
|
||||
@@ -112,12 +113,15 @@ class ChatPreferencesLayoutPersistenceTest {
|
||||
assertFalse(writtenXml.contains("UNSAVED-CALL"));
|
||||
assertTrue(writtenXml.contains("<futureExtension mode=\"keep-me\">"));
|
||||
assertTrue(writtenXml.contains("<futureLayoutValue>untouched</futureLayoutValue>"));
|
||||
assertTrue(writtenXml.contains("<configVersion>6</configVersion>"));
|
||||
assertTrue(writtenXml.contains("<configVersion>7</configVersion>"));
|
||||
assertTrue(writtenXml.contains("<GUIscn_ChatwindowMainSceneSizeHW>812.0;1340.0"));
|
||||
assertTrue(writtenXml.contains("<GUIstationMapClusteringEnabled>false"
|
||||
+ "</GUIstationMapClusteringEnabled>"));
|
||||
|
||||
ChatPreferences restored = preferencesAt(preferencesFile);
|
||||
assertTrue(restored.readPreferencesFromXmlFile());
|
||||
assertEquals("SAVED-CALL", restored.getStn_loginCallSign());
|
||||
assertFalse(restored.isGUIstationMapClusteringEnabled());
|
||||
assertEquals(123.75,
|
||||
restored.getTableColumnWidth("qso-other-monitor", "call-tx").orElseThrow());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.model.ChatPreferences;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ChatPreferencesStationMapClusteringTest {
|
||||
|
||||
@TempDir
|
||||
Path temporaryDirectory;
|
||||
|
||||
@Test
|
||||
void clusteringIsEnabledByDefault() {
|
||||
assertTrue(new ChatPreferences().isGUIstationMapClusteringEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void disabledClusteringSurvivesFullXmlRoundTrip() throws IOException {
|
||||
Path preferencesFile = temporaryDirectory.resolve("preferences.xml");
|
||||
ChatPreferences written = preferencesAt(preferencesFile);
|
||||
written.setGUIstationMapClusteringEnabled(false);
|
||||
|
||||
assertTrue(written.writePreferencesToXmlFile());
|
||||
|
||||
String writtenXml = Files.readString(preferencesFile);
|
||||
assertTrue(writtenXml.contains("<configVersion>7</configVersion>"));
|
||||
assertTrue(writtenXml.contains("<GUIstationMapClusteringEnabled>false"
|
||||
+ "</GUIstationMapClusteringEnabled>"));
|
||||
|
||||
ChatPreferences restored = preferencesAt(preferencesFile);
|
||||
assertTrue(restored.readPreferencesFromXmlFile());
|
||||
assertFalse(restored.isGUIstationMapClusteringEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void versionSixWithoutClusteringSettingKeepsClusteringEnabled() throws IOException {
|
||||
Path preferencesFile = temporaryDirectory.resolve("version-six.xml");
|
||||
Files.writeString(preferencesFile, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<praktiKST>
|
||||
<configVersion>6</configVersion>
|
||||
<guiOptions>
|
||||
<GUIstationMapStageSceneSizeHW>1000.0;800.0</GUIstationMapStageSceneSizeHW>
|
||||
</guiOptions>
|
||||
</praktiKST>
|
||||
""");
|
||||
|
||||
ChatPreferences restored = preferencesAt(preferencesFile);
|
||||
restored.setGUIstationMapClusteringEnabled(false);
|
||||
assertTrue(restored.readPreferencesFromXmlFile());
|
||||
assertTrue(restored.isGUIstationMapClusteringEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingGuiOptionsKeepsClusteringEnabled() throws IOException {
|
||||
Path preferencesFile = temporaryDirectory.resolve("missing.xml");
|
||||
Files.writeString(preferencesFile, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<praktiKST>
|
||||
<configVersion>6</configVersion>
|
||||
</praktiKST>
|
||||
""");
|
||||
|
||||
ChatPreferences restored = preferencesAt(preferencesFile);
|
||||
restored.setGUIstationMapClusteringEnabled(false);
|
||||
assertTrue(restored.readPreferencesFromXmlFile());
|
||||
assertTrue(restored.isGUIstationMapClusteringEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidClusteringValueKeepsClusteringEnabled() throws IOException {
|
||||
Path preferencesFile = temporaryDirectory.resolve("invalid.xml");
|
||||
Files.writeString(preferencesFile, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<praktiKST>
|
||||
<configVersion>7</configVersion>
|
||||
<guiOptions>
|
||||
<GUIstationMapClusteringEnabled>sometimes</GUIstationMapClusteringEnabled>
|
||||
</guiOptions>
|
||||
</praktiKST>
|
||||
""");
|
||||
|
||||
ChatPreferences restored = preferencesAt(preferencesFile);
|
||||
restored.setGUIstationMapClusteringEnabled(false);
|
||||
assertTrue(restored.readPreferencesFromXmlFile());
|
||||
assertTrue(restored.isGUIstationMapClusteringEnabled());
|
||||
}
|
||||
|
||||
private ChatPreferences preferencesAt(Path preferencesFile) {
|
||||
ChatPreferences preferences = new ChatPreferences();
|
||||
preferences.setStoreAndRestorePreferencesFileName(preferencesFile.toString());
|
||||
return preferences;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.view.map.MapHtmlResources;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class MapHtmlResourcesContractTest {
|
||||
|
||||
@Test
|
||||
void stationClusteringCanBeToggledWithoutReplacingStationData() {
|
||||
String html = MapHtmlResources.createStationMapHtml(12345);
|
||||
|
||||
assertTrue(html.contains("let stationClusteringEnabled = true;"));
|
||||
assertTrue(html.contains("if (!stationClusteringEnabled"));
|
||||
assertTrue(html.contains("|| Number(map.getZoom()) >= KST_CLUSTER_DISABLE_ZOOM)"));
|
||||
assertTrue(html.contains("function setStationClusteringEnabled(enabled)"));
|
||||
|
||||
int setterStart = html.indexOf("function setStationClusteringEnabled(enabled)");
|
||||
int setterEnd = html.indexOf('}', setterStart);
|
||||
String setterBody = html.substring(setterStart, setterEnd);
|
||||
int stateUpdate = setterBody.indexOf("stationClusteringEnabled = Boolean(enabled);");
|
||||
int markerRender = setterBody.indexOf("renderStationMarkers();");
|
||||
|
||||
assertTrue(stateUpdate >= 0);
|
||||
assertTrue(markerRender > stateUpdate);
|
||||
assertFalse(setterBody.contains("stationData ="));
|
||||
assertTrue(html.contains("setStationClusteringEnabled: setStationClusteringEnabled"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user