mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-09-10 19:25:32 +02:00
Add optional station map clustering toggle (solves #79) and added documentation
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# KST4Contest Project Context
|
||||
|
||||
Last reviewed: 2026-09-02
|
||||
Last reviewed: 2026-09-03
|
||||
|
||||
This file is the durable technical project context for KST4Contest. It is not a user manual and not a replacement for the changelog. Current code, tests and authoritative external specifications remain the source of truth when this document is stale or ambiguous.
|
||||
|
||||
@@ -52,7 +52,8 @@ JavaFX ObservableList / UI state
|
||||
|
||||
## Configuration and Layout Persistence
|
||||
|
||||
- `preferences.xml` configuration version 6 stores optional managed leaf-column widths below `guiOptions`, identified by stable table and column IDs. Parent-column widths remain derived from their leaf columns.
|
||||
- The current `preferences.xml` configuration version is 7. Version 6 introduced optional managed leaf-column widths below `guiOptions`, identified by stable table and column IDs. Parent-column widths remain derived from their leaf columns.
|
||||
- `GUIstationMapClusteringEnabled` is a layout preference below `guiOptions`. It defaults to `true`, is selectively autosaved and controls only screen-based clustering of nearby map markers. Missing or malformed values retain the enabled default for backward compatibility.
|
||||
- Stored widths take precedence. Without a usable entry, a managed column is sized once when meaningful table data first becomes available. Message and similar free-text columns use a flexible initial width instead of following the longest value.
|
||||
- Main-window and separate-monitor DXCluster/QSO tables use distinct layout IDs even though they share the underlying message stores.
|
||||
- Window sizes and positions, relevant divider positions and managed column widths are selectively autosaved after a short debounce. A pending write is flushed during application shutdown.
|
||||
@@ -126,6 +127,8 @@ CR/LF framing, XML framing, ports/transports, callsign normalization and frequen
|
||||
- Contest operating speed and low-friction interaction are primary goals.
|
||||
- Incidental code changes must not unexpectedly change selection, focus, sorting, tab state, map zoom or prefilled text.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -184,6 +187,7 @@ Before implementing planned items, re-check current decisions and obtain a fresh
|
||||
- Historical project context is useful but may be stale; current code/tests win.
|
||||
- External service/API behaviour must be verified against current upstream documentation when uncertain.
|
||||
- Screenshots in manuals/website may need targeted replacement after visible UI changes; never fabricate them.
|
||||
- `station_map_path_analysis.png` and `station_map_reset.png` predate the **Group nearby stations** checkbox in the map header. Replace them with current screenshots when suitable source images are available; the website reuses `station_map_path_analysis.png` through `/manual/assets/`.
|
||||
|
||||
## Recent Significant Changes
|
||||
|
||||
|
||||
@@ -335,7 +335,11 @@ Ein einzelner Stationsmarker kann direkt angeklickt werden. KST4Contest:
|
||||
3. aktualisiert den **Further Info**-Bereich und
|
||||
4. bereitet das vollständige sichtbare Rufzeichen als `/cq`-Empfänger vor.
|
||||
|
||||
Marker, die bei der aktuellen Zoomstufe zu dicht beieinanderliegen, werden als Cluster mit einer Stationsanzahl dargestellt. Ein Klick auf einen Cluster vergrößert den betreffenden Kartenausschnitt. Erst ein anschließend sichtbarer einzelner Marker wählt eine konkrete Station aus.
|
||||
Ist **Group nearby stations** ausgewählt, werden Marker, die bei niedrigen Zoomstufen zu dicht beieinanderliegen, als Cluster mit einer Stationsanzahl dargestellt. Ein Klick auf einen Cluster vergrößert den betreffenden Kartenausschnitt. Erst ein anschließend sichtbarer einzelner Marker wählt eine konkrete Station aus. Wird die Checkbox ausgeschaltet, zeigt die Karte unabhängig von der Zoomstufe alle positionierbaren Stationen als einzelne Marker.
|
||||
|
||||
Das Umschalten wirkt sofort und verändert weder Zoom noch Kartenausschnitt oder Stationsauswahl. Die Einstellung wird automatisch gespeichert und beim nächsten Programmstart wiederhergestellt. Ohne gespeicherte Einstellung bleibt **Group nearby stations** ausgewählt, damit bestehende Installationen zunächst das bisherige Verhalten behalten.
|
||||
|
||||
Der Schalter betrifft nur die räumlichen Cluster auf dem Bildschirm. Aktive Chatvarianten desselben normalisierten Basisrufzeichens können weiterhin einen gemeinsamen geografischen Marker verwenden und bleiben unabhängig davon getrennte Nachrichtenziele.
|
||||
|
||||
Die Kopfzeile ergänzt bei ausgewählter Station:
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ v1.42 führt mehrere bisher getrennte Auswertungen zusammen. Bandinformationen,
|
||||
|
||||
- **Filter zurücksetzen:** Ein eigener Reset-Button entfernt die aktiven Filterprädikate der Benutzerliste zuverlässig.
|
||||
|
||||
- **Kartencluster:** Räumlich dicht beieinanderliegende Stationen werden bei niedrigen Zoomstufen zusammengefasst. Die ausgewählte Station und relevante Richtungsgelegenheiten bleiben einzeln sichtbar.
|
||||
- **Optionale Kartencluster:** **Group nearby stations** schaltet die räumliche Gruppierung bei niedrigen Zoomstufen unmittelbar ein oder aus. Die automatisch gespeicherte Auswahl verändert weder Kartenausschnitt noch Stationsauswahl; die Zusammenfassung aktiver Varianten desselben Basisrufzeichens bleibt davon unabhängig. Damit ist [Issue #79](https://github.com/praktimarc/kst4contest/issues/79) umgesetzt.
|
||||
|
||||
- **Ausblendbare Streckenanalyse:** Geländeprofil und Analysebereich der Stationskarte können vollständig ausgeblendet werden. Die Auswahl wird gespeichert und beim nächsten Programmstart wiederhergestellt.
|
||||
|
||||
|
||||
@@ -831,7 +831,11 @@ Die Bandangaben verwenden dieselbe Herleitung wie die Bandspalten, der Filter **
|
||||
|
||||
Treffen mehrere Zustände gleichzeitig zu, hat die für den Betrieb wichtigere Markierung Vorrang. Eine ausgewählte Station bleibt deshalb orange; eine Richtungsgelegenheit wird grün dargestellt, auch wenn das Rufzeichen bereits gearbeitet wurde.
|
||||
|
||||
Bei niedrigen Zoomstufen werden räumlich dicht beieinanderliegende Stationen zu einem Cluster zusammengefasst. Die Zahl im Cluster gibt die Anzahl der enthaltenen Stationen an. Ein Klick zoomt weiter hinein, wählt aber noch keine einzelne Station aus. Die aktuell ausgewählte Station und grün markierte Richtungsgelegenheiten bleiben auch bei niedriger Zoomstufe als einzelne Marker sichtbar.
|
||||
Mit der Checkbox **Group nearby stations** lässt sich die räumliche Gruppierung steuern. Ist sie ausgewählt, werden bei niedrigen Zoomstufen dicht beieinanderliegende Marker zu einem Bildschirm-Cluster zusammengefasst. Die Zahl im Cluster gibt die Anzahl der enthaltenen Stationen an. Ein Klick zoomt weiter hinein, wählt aber noch keine einzelne Station aus. Die aktuell ausgewählte Station und grün markierte Richtungsgelegenheiten bleiben auch bei niedriger Zoomstufe möglichst als einzelne Marker sichtbar.
|
||||
|
||||
Ist **Group nearby stations** nicht ausgewählt, erscheinen alle positionierbaren Stationen unabhängig von der Zoomstufe als einzelne Marker. Die Änderung wirkt sofort, ohne Stationsdaten neu zu laden oder Zoom, Kartenausschnitt und Auswahl zu verändern. KST4Contest speichert die Einstellung automatisch und stellt sie beim nächsten Start wieder her. Bestehende Installationen beginnen mit aktivierter Gruppierung und behalten damit zunächst das bisherige Verhalten.
|
||||
|
||||
Diese Bildschirm-Cluster sind nicht mit der Zusammenfassung von Chatvarianten zu verwechseln. Mehrere aktive Varianten desselben normalisierten Basisrufzeichens werden weiterhin zu einem geografischen Marker zusammengeführt. Das Ausschalten von **Group nearby stations** erzeugt daraus keine zusätzlichen Marker und verändert weder Chatidentitäten noch Filter oder Stationsdaten.
|
||||
|
||||
### Auswahl und geografische Hilfen
|
||||
|
||||
|
||||
@@ -886,10 +886,11 @@ Der Dark Mode wird über **Windows → Use dark mode design** aktiviert. Mit **W
|
||||
|
||||
## Einstellungen speichern
|
||||
|
||||
**Save Settings** speichert die fachlichen Einstellungen und den vollständigen aktuellen Layoutstand. Änderungen an Fenstergrößen und -positionen, relevanten Dividern sowie verwalteten Tabellenbreiten werden zusätzlich automatisch mit kurzer Verzögerung gespeichert. Ein ausstehender Layoutstand wird beim Programmende noch geschrieben.
|
||||
**Save Settings** speichert die fachlichen Einstellungen und den vollständigen aktuellen Layoutstand. Änderungen an Fenstergrößen und -positionen, relevanten Dividern, verwalteten Tabellenbreiten sowie der Karteneinstellung **Group nearby stations** werden zusätzlich automatisch mit kurzer Verzögerung gespeichert. Ein ausstehender Layoutstand wird beim Programmende noch geschrieben.
|
||||
|
||||
- Speicherort: unter Linux und macOS `~/.praktiKST/preferences.xml` und unter Windows `%USERPROFILE%\.praktiKST\preferences.xml` (bzw. `C:\Users\<Benutzername>\.praktiKST\preferences.xml`)
|
||||
- Der automatische Layout-Writer übernimmt keine noch nicht mit **Save Settings** bestätigten fachlichen Änderungen.
|
||||
- Die Konfigurationsversion 6 ergänzt optionale Spaltenbreiten unter `guiOptions`. Ältere `preferences.xml`-Dateien bleiben lesbar; fehlen Breiten oder sind Einträge ungültig, ermittelt KST4Contest wieder brauchbare Anfangsbreiten.
|
||||
- Ältere Programmversionen ignorieren die zusätzlichen XML-Einträge. Wenn eine ältere Version die Datei vollständig neu speichert, können lediglich die Spaltenbreiten verloren gehen.
|
||||
- Die Konfigurationsversion 7 ergänzt `GUIstationMapClusteringEnabled` unter `guiOptions`. Fehlt der Eintrag oder ist sein Wert unbrauchbar, bleibt die räumliche Kartengruppierung aktiviert.
|
||||
- Ältere Programmversionen ignorieren die zusätzlichen XML-Einträge. Wenn eine ältere Version die Datei vollständig neu speichert, können die Spaltenbreiten und die gespeicherte Auswahl für **Group nearby stations** verloren gehen.
|
||||
- Bei Problemen: Konfigurationsdatei löschen → KST4Contest erstellt eine neue mit Standardwerten.
|
||||
|
||||
@@ -38,7 +38,7 @@ v1.42 brings several previously separate calculations together. Band information
|
||||
|
||||
- **Filter reset:** A dedicated reset button reliably removes the active user-list filter predicates.
|
||||
|
||||
- **Map clustering:** Stations close to each other are grouped at lower zoom levels. The selected station and relevant direction opportunities remain individually visible.
|
||||
- **Optional map clustering:** **Group nearby stations** immediately enables or disables spatial grouping at lower zoom levels. The automatically stored setting changes neither the viewport nor the selected station; aggregation of active variants sharing one base callsign remains independent. This implements [Issue #79](https://github.com/praktimarc/kst4contest/issues/79).
|
||||
|
||||
- **Hideable path analysis:** The terrain profile and analysis section of the station map can be hidden completely. The selected state is stored and restored at the next application start.
|
||||
|
||||
|
||||
@@ -943,10 +943,11 @@ Enable Dark Mode through **Windows → Use dark mode design**. Use **Windows →
|
||||
|
||||
## Saving Settings
|
||||
|
||||
**Save Settings** stores functional settings and the complete current layout. Changes to window sizes and positions, relevant dividers and managed table-column widths are also saved automatically after a short delay. Any pending layout update is written when the programme exits.
|
||||
**Save Settings** stores functional settings and the complete current layout. Changes to window sizes and positions, relevant dividers, managed table-column widths and the **Group nearby stations** map setting are also saved automatically after a short delay. Any pending layout update is written when the programme exits.
|
||||
|
||||
- Storage location: `~/.praktiKST/preferences.xml` on Linux and macOS and `%USERPROFILE%\.praktiKST\preferences.xml` (or `C:\Users\<Username>\.praktiKST\preferences.xml`) on Windows
|
||||
- The automatic layout writer does not copy functional changes which have not yet been confirmed with **Save Settings**.
|
||||
- Configuration version 6 adds optional column-width entries below `guiOptions`. Older `preferences.xml` files remain readable. Missing or invalid widths simply cause KST4Contest to calculate useful initial widths again.
|
||||
- Older programme versions ignore the additional XML entries. If an older version rewrites the complete file, only the column widths may be lost.
|
||||
- Configuration version 7 adds `GUIstationMapClusteringEnabled` below `guiOptions`. If the entry is missing or unusable, spatial map clustering remains enabled.
|
||||
- Older programme versions ignore the additional XML entries. If an older version rewrites the complete file, column widths and the stored **Group nearby stations** choice may be lost.
|
||||
- If you encounter problems: delete the configuration file → KST4Contest will create a new one with default values.
|
||||
|
||||
@@ -825,7 +825,11 @@ Marker colours provide a compact status indication:
|
||||
|
||||
The selected state has the highest display priority, followed by the directional warning and Worked state. A selected station therefore remains orange even if it also meets one of the other conditions.
|
||||
|
||||
At lower zoom levels, nearby markers are combined into screen-based clusters. This is a display function and does not merge the underlying chat members. Selected stations and important directional candidates remain individually visible where possible.
|
||||
The **Group nearby stations** checkbox controls spatial grouping. When selected, nearby markers are combined into screen-based clusters at lower zoom levels. The number inside a cluster shows how many stations it contains. Clicking a cluster zooms in but does not select an individual station. Selected stations and important directional candidates remain individually visible where possible.
|
||||
|
||||
Clearing **Group nearby stations** displays every positionable station as an individual marker at every zoom level. The change takes effect immediately without reloading station data or changing the current zoom, viewport or selection. KST4Contest saves the setting automatically and restores it at the next start. Existing installations initially keep clustering enabled and therefore retain the previous behaviour.
|
||||
|
||||
Screen-based clustering is separate from base-callsign aggregation. Active variants of the same normalised base callsign continue to share one geographical marker where applicable. Disabling **Group nearby stations** does not split that marker and does not change chat identities, filters or station data.
|
||||
|
||||
Clicking a station marker selects the corresponding active chat member in the main window. KST4Contest scrolls to the entry in the user list, updates the **Further Info** panel and prepares the complete visible callsign as the message target. The chat suffix and category therefore remain relevant even though several variants may share one map marker.
|
||||
|
||||
|
||||
@@ -339,7 +339,11 @@ A single station marker can be selected directly. KST4Contest then:
|
||||
|
||||
Chat logins with the same normalised base callsign and position may share one marker. They nevertheless remain separate message targets inside KST4Contest.
|
||||
|
||||
Markers which are too close together at the current zoom level are displayed as a cluster containing the number of stations. Clicking the cluster zooms into that area. A concrete station is selected only after an individual marker becomes visible and is clicked.
|
||||
When **Group nearby stations** is selected, markers which are too close together at lower zoom levels are displayed as a cluster containing the number of stations. Clicking the cluster zooms into that area. A concrete station is selected only after an individual marker becomes visible and is clicked. Clearing the checkbox displays every positionable station as an individual marker at every zoom level.
|
||||
|
||||
The change takes effect immediately without changing the current zoom, viewport or station selection. It is saved automatically and restored at the next programme start. If no value has been stored yet, **Group nearby stations** remains selected so existing installations retain the previous behaviour.
|
||||
|
||||
This switch controls only the spatial clusters on screen. Active chat variants of the same normalised base callsign may still share one geographical marker and remain separate message targets regardless of this setting.
|
||||
|
||||
For a selected station, the header additionally shows:
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,8 @@ The map retains the context already known by KST4Contest:
|
||||
|
||||
Only stations with a usable six-character locator can be positioned. Active chat variants of the same normalised base callsign are combined into one marker, while their actual message destinations remain separate.
|
||||
|
||||
At lower zoom levels, **Group nearby stations** keeps dense map areas readable by combining close markers into screen-based clusters. The grouping can be disabled immediately without changing the current viewport or selection, and KST4Contest remembers the choice. This setting does not split the geographical marker shared by active variants of the same base callsign.
|
||||
|
||||
Green has a specific meaning: it marks a directional opportunity derived from directed ON4KST messages. It does not merely mean that the marker happens to lie inside the local antenna sector.
|
||||
|
||||
## Selection remains connected to the chat
|
||||
|
||||
Reference in New Issue
Block a user