diff --git a/github_docs/de-Funktionen.md b/github_docs/de-Funktionen.md
index 033536f..3f7ec1a 100644
--- a/github_docs/de-Funktionen.md
+++ b/github_docs/de-Funktionen.md
@@ -496,8 +496,10 @@ So kann der Contest-Operator auf einem Blick sehen, welche Stationen wann und ü
## Intervall-Beacon
KST4Contest kann wiederkehrende CQ-Nachrichten in den öffentlichen Chat senden. Beide Chat-Kategorien verwenden ein gemeinsames Intervall, besitzen aber jeweils einen eigenen Aktivierungsschalter und Nachrichtentext. Globale Variablen wie `MYQRG`, `SECONDQRG` oder `MYLOCATOR` werden unmittelbar vor jeder Aussendung aktualisiert.
+
Der Beacon ist für längeres CQ-Rufen auf einer festen Frequenz gedacht. Beim Absuchen oder häufigen Wechseln der QRG sollte er ausgeschaltet werden, damit keine inzwischen falsche Frequenz verbreitet wird. Details: [Konfiguration – Beacon Settings](de-Konfiguration#beacon-settings-automatischer-beacon).
+
---
## Simplelogfile
@@ -510,7 +512,7 @@ Dateibasierte Log-Auswertung per Regex. Details: [Log-Synchronisation](Log-Synch
Ein separates Fenster zeigt den QSO-Fluss zwischen anderen Stationen. Besonders interessant in ruhigeren Nacht-Stunden während des Contests, wenn weniger Verkehr herrscht.
-Dieses Fenster kann miniaturisiert werden, wenn es nicht benötigt wird. Zukünftig geplant: Filterung auf Stationen im ausgewählten QTF.
+Dieses Fenster kann minimiert werden, wenn es nicht benötigt wird. Zukünftig geplant: Filterung auf Stationen im ausgewählten QTF.
---
diff --git a/src/main/java/kst4contest/model/ChatPreferences.java b/src/main/java/kst4contest/model/ChatPreferences.java
index aca0a4b..c780f5a 100644
--- a/src/main/java/kst4contest/model/ChatPreferences.java
+++ b/src/main/java/kst4contest/model/ChatPreferences.java
@@ -50,7 +50,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 = 4;
+ public static final int CONFIG_VERSION = 5;
// Prefer writing tag names that mirror variable names (human readable). Keep legacy tags for compatibility.
private static final String TAG_CONFIG_VERSION = "configVersion";
@@ -339,6 +339,7 @@ public class ChatPreferences {
private double[] GUIstationMapStageSceneSizeHW = new double[] { 1000, 800 };
private double[] GUIstationMapStagePositionXY = new double[] { Double.NaN, Double.NaN };
+ private boolean GUIstationMapPathAnalysisVisible = true;
/*********************************************************************************
@@ -635,6 +636,14 @@ public class ChatPreferences {
this.GUIstationMapStagePositionXY = GUIstationMapStagePositionXY;
}
+ public boolean isGUIstationMapPathAnalysisVisible() {
+ return GUIstationMapPathAnalysisVisible;
+ }
+
+ public void setGUIstationMapPathAnalysisVisible(boolean GUIstationMapPathAnalysisVisible) {
+ this.GUIstationMapPathAnalysisVisible = GUIstationMapPathAnalysisVisible;
+ }
+
public boolean isGuiOptions_defaultFilterNothing() {
return guiOptions_defaultFilterNothing;
}
@@ -2048,6 +2057,12 @@ public class ChatPreferences {
);
guiOptions.appendChild(GUIstationMapStagePositionXY);
+ Element GUIstationMapPathAnalysisVisible = doc.createElement("GUIstationMapPathAnalysisVisible");
+ GUIstationMapPathAnalysisVisible.setTextContent(
+ String.valueOf(this.isGUIstationMapPathAnalysisVisible())
+ );
+ guiOptions.appendChild(GUIstationMapPathAnalysisVisible);
+
/****************************************************************************************
****************************** now write this XML! *************************************
****************************************************************************************/
@@ -2783,6 +2798,17 @@ public class ChatPreferences {
this.getGUIstationMapStagePositionXY()
);
+ /*
+ * Files written before config version 5 do not contain this value.
+ * Keep the default true in that case so existing users discover the
+ * path-analysis feature before choosing to hide it themselves.
+ */
+ this.setGUIstationMapPathAnalysisVisible(getBoolean(
+ element,
+ this.isGUIstationMapPathAnalysisVisible(),
+ "GUIstationMapPathAnalysisVisible"
+ ));
+
// Splitpane divider positions
String s1 = getText(element, null, "GUIselectedCallSignSplitPane_dividerposition");
if (s1 != null) {
diff --git a/src/main/java/kst4contest/test/ChatPreferencesStationMapVisibilityTest.java b/src/main/java/kst4contest/test/ChatPreferencesStationMapVisibilityTest.java
new file mode 100644
index 0000000..23f8291
--- /dev/null
+++ b/src/main/java/kst4contest/test/ChatPreferencesStationMapVisibilityTest.java
@@ -0,0 +1,73 @@
+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 ChatPreferencesStationMapVisibilityTest {
+
+ @TempDir
+ Path temporaryDirectory;
+
+ @Test
+ void pathAnalysisIsVisibleByDefault() {
+ ChatPreferences preferences = new ChatPreferences();
+
+ assertTrue(preferences.isGUIstationMapPathAnalysisVisible());
+ }
+
+ @Test
+ void hiddenPathAnalysisStateSurvivesXmlRoundTrip() throws IOException {
+ Path preferencesFile = temporaryDirectory.resolve("preferences.xml");
+
+ ChatPreferences writtenPreferences = new ChatPreferences();
+ writtenPreferences.setStoreAndRestorePreferencesFileName(
+ preferencesFile.toString());
+ writtenPreferences.setGUIstationMapPathAnalysisVisible(false);
+ writtenPreferences.writePreferencesToXmlFile();
+
+ String writtenXml = Files.readString(preferencesFile);
+ assertTrue(writtenXml.contains(
+ "false"
+ + ""));
+
+ ChatPreferences restoredPreferences = new ChatPreferences();
+ restoredPreferences.setStoreAndRestorePreferencesFileName(
+ preferencesFile.toString());
+ restoredPreferences.readPreferencesFromXmlFile();
+
+ assertFalse(restoredPreferences.isGUIstationMapPathAnalysisVisible());
+ }
+
+ @Test
+ void legacyXmlWithoutVisibilitySettingKeepsAnalysisDiscoverable()
+ throws IOException {
+
+ Path legacyPreferencesFile =
+ temporaryDirectory.resolve("legacy-preferences.xml");
+
+ Files.writeString(legacyPreferencesFile, """
+
+
+ 4
+
+ 1000.0;800.0
+
+
+ """);
+
+ ChatPreferences restoredPreferences = new ChatPreferences();
+ restoredPreferences.setStoreAndRestorePreferencesFileName(
+ legacyPreferencesFile.toString());
+ restoredPreferences.readPreferencesFromXmlFile();
+
+ assertTrue(restoredPreferences.isGUIstationMapPathAnalysisVisible());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/kst4contest/view/map/StationMapView.java b/src/main/java/kst4contest/view/map/StationMapView.java
index f42d37a..ab725dc 100644
--- a/src/main/java/kst4contest/view/map/StationMapView.java
+++ b/src/main/java/kst4contest/view/map/StationMapView.java
@@ -28,6 +28,8 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.ColumnConstraints;
+import javafx.geometry.Pos;
+import javafx.scene.layout.HBox;
/**
* Standalone station map window.
@@ -49,6 +51,9 @@ public final class StationMapView {
*/
private static final boolean MAP_DEBUG_LOGGING = false;
+ private static final double MINIMUM_HEIGHT_WITH_PATH_ANALYSIS = 650.0;
+ private static final double MINIMUM_HEIGHT_WITHOUT_PATH_ANALYSIS = 420.0;
+
private final PathProfileChart detailPathProfileChart = new PathProfileChart();
private final Label detailPathModeValue = new Label("-");
@@ -76,6 +81,12 @@ public final class StationMapView {
private VBox detailPane;
private final Label statusLabel = new Label("Station map not initialized yet.");
+
+ private final Label pathAnalysisHiddenHintLabel = new Label("Path analysis is hidden.");
+ private final Button pathAnalysisVisibilityButton = new Button();
+ private final Tooltip pathAnalysisVisibilityTooltip = new Tooltip();
+
+
private final Label detailCallsignValue = new Label("-");
private final Label detailLocatorValue = new Label("-");
private final Label detailQrbValue = new Label("-");
@@ -138,8 +149,12 @@ public final class StationMapView {
private PathAnalysisResult lastPathAnalysisResult = PathAnalysisResult.waitingForSelection("");
private VBox mapAndProfilePane;
+ private VBox profileSection;
+ private VBox pathAnalysisSection;
private ScrollPane detailScrollPane;
+
+
private final Label detailPathLosValue = new Label("-");
private final Label detailPathWorstClearanceValue = new Label("-");
@@ -287,6 +302,14 @@ public final class StationMapView {
}
});
+ pathAnalysisVisibilityButton.setMinWidth(Region.USE_PREF_SIZE);
+ pathAnalysisVisibilityButton.setTooltip(pathAnalysisVisibilityTooltip);
+ pathAnalysisVisibilityButton.setOnAction(event ->
+ setPathAnalysisVisible(!profileSection.isVisible(), true));
+
+ pathAnalysisHiddenHintLabel.setMinWidth(Region.USE_PREF_SIZE);
+ pathAnalysisHiddenHintLabel.setStyle("-fx-font-style: italic; -fx-opacity: 0.85;");
+
webView.setFocusTraversable(true);
webView.setPickOnBounds(true);
@@ -323,7 +346,8 @@ public final class StationMapView {
webView.widthProperty().addListener((obs, oldValue, newValue) -> requestMapInvalidateSize());
webView.heightProperty().addListener((obs, oldValue, newValue) -> requestMapInvalidateSize());
- VBox profileSection = createProfileSection();
+ profileSection = createProfileSection();
+
mapAndProfilePane = new VBox(6, webView, profileSection);
mapAndProfilePane.setPadding(new Insets(0));
@@ -349,10 +373,9 @@ public final class StationMapView {
mapAndProfilePane.widthProperty().subtract(20)
);
- detailPane = new VBox(10,
- createSelectedStationSection(),
- createPathAnalysisSection()
- );
+ pathAnalysisSection = createPathAnalysisSection();
+ detailPane = new VBox(10, createSelectedStationSection(), pathAnalysisSection);
+
detailPane.setPadding(new Insets(10));
detailPane.setMinWidth(0);
@@ -364,8 +387,14 @@ public final class StationMapView {
detailScrollPane.setFitToWidth(true);
detailScrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
detailScrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
- detailScrollPane.setMinWidth(300);
+ /*
+ * Allow the details pane to be reduced far enough to leave more room for the
+ * map. At its minimum width, a callsign with up to ten characters remains
+ * readable.
+ */
+ detailScrollPane.setMinWidth(210);
detailScrollPane.setPrefWidth(350);
+
detailScrollPane.setMaxWidth(Double.MAX_VALUE);
detailScrollPane.setFitToWidth(true);
@@ -378,8 +407,7 @@ public final class StationMapView {
SplitPane.setResizableWithParent(detailScrollPane, true);
rootPane = new BorderPane();
- rootPane.setTop(statusLabel);
- BorderPane.setMargin(statusLabel, new Insets(8));
+ rootPane.setTop(createMapHeader());
rootPane.setCenter(mainSplitPane);
double[] size = chatPreferences.getGUIstationMapStageSceneSizeHW();
@@ -390,7 +418,12 @@ public final class StationMapView {
scene = new Scene(rootPane, initialWidth, initialHeight);
stage.setMinWidth(900);
- stage.setMinHeight(650);
+ stage.setMinHeight(resolveMinimumStationMapHeight(
+ chatPreferences.isGUIstationMapPathAnalysisVisible()));
+
+ stage.setScene(scene);
+ setPathAnalysisVisible(chatPreferences.isGUIstationMapPathAnalysisVisible(), false);
+ applyThemeFromPreferences();
stage.setScene(scene);
applyThemeFromPreferences();
@@ -446,6 +479,91 @@ public final class StationMapView {
}
+ /**
+ * Creates an always-visible header for the map status and analysis controls.
+ *
+ * Keeping the control outside the sections that it hides is important: users
+ * must always have an obvious way to restore a previously hidden analysis.
+ */
+ private HBox createMapHeader() {
+ // The status may be shortened before the show/hide control is ever clipped.
+ statusLabel.setMinWidth(0);
+ statusLabel.setMaxWidth(Double.MAX_VALUE);
+ statusLabel.setTextOverrun(OverrunStyle.ELLIPSIS);
+
+ HBox header = new HBox(
+ 10,
+ statusLabel,
+ pathAnalysisHiddenHintLabel,
+ pathAnalysisVisibilityButton
+ );
+ header.setAlignment(Pos.CENTER_LEFT);
+ header.setPadding(new Insets(8));
+ HBox.setHgrow(statusLabel, Priority.ALWAYS);
+ return header;
+ }
+
+ /**
+ * Shows or hides both parts of the path analysis as one logical feature.
+ *
+ * Both visible and managed must be changed. A node that is merely invisible
+ * would still reserve layout space and the map would not grow into that area.
+ * The current analysis result remains attached to the controls and is
+ * immediately available again when the user restores the sections.
+ *
+ * @param visible true to show the profile and detailed analysis
+ * @param persist true when the change was explicitly requested by the user
+ */
+ private void setPathAnalysisVisible(boolean visible, boolean persist) {
+ profileSection.setVisible(visible);
+ profileSection.setManaged(visible);
+
+ pathAnalysisSection.setVisible(visible);
+ pathAnalysisSection.setManaged(visible);
+
+ pathAnalysisHiddenHintLabel.setVisible(!visible);
+ pathAnalysisHiddenHintLabel.setManaged(!visible);
+
+ pathAnalysisVisibilityButton.setText(
+ visible ? "Hide path analysis" : "Show path analysis");
+ pathAnalysisVisibilityButton.setAccessibleText(
+ visible ? "Hide path analysis" : "Show path analysis");
+ pathAnalysisVisibilityButton.setStyle(
+ visible ? "" : "-fx-font-weight: bold;");
+
+ pathAnalysisVisibilityTooltip.setText(visible
+ ? "Hide the path profile and detailed path analysis. You can show them again at any time."
+ : "Show the path profile and detailed path analysis.");
+
+ pathAnalysisVisibilityButton.setAccessibleHelp(
+ pathAnalysisVisibilityTooltip.getText());
+
+ if (!visible) {
+ /*
+ * Do not leave a profile hover marker on the map after its chart was
+ * hidden.
+ */
+ showProfileHoverPointOnMap(null);
+ }
+
+ stage.setMinHeight(resolveMinimumStationMapHeight(visible));
+
+ if (persist) {
+ chatPreferences.setGUIstationMapPathAnalysisVisible(visible);
+ }
+
+ if (rootPane != null) {
+ rootPane.requestLayout();
+ Platform.runLater(this::requestMapInvalidateSize);
+ }
+ }
+
+ private double resolveMinimumStationMapHeight(boolean pathAnalysisVisible) {
+ return pathAnalysisVisible
+ ? MINIMUM_HEIGHT_WITH_PATH_ANALYSIS
+ : MINIMUM_HEIGHT_WITHOUT_PATH_ANALYSIS;
+ }
+
private VBox createSelectedStationSection() {
GridPane detailGrid = new GridPane();
detailGrid.setHgap(8);
@@ -494,13 +612,18 @@ public final class StationMapView {
gridPane.getColumnConstraints().clear();
ColumnConstraints labelColumn = new ColumnConstraints();
- labelColumn.setMinWidth(105);
+ labelColumn.setMinWidth(70);
labelColumn.setPrefWidth(115);
labelColumn.setMaxWidth(130);
labelColumn.setHgrow(Priority.NEVER);
ColumnConstraints valueColumn = new ColumnConstraints();
- valueColumn.setMinWidth(180);
+
+ /*
+ * Reserve enough space for a callsign with up to ten characters, while still
+ * allowing the details pane to become considerably narrower.
+ */
+ valueColumn.setMinWidth(85);
valueColumn.setHgrow(Priority.ALWAYS);
gridPane.getColumnConstraints().addAll(labelColumn, valueColumn);
@@ -1530,8 +1653,11 @@ public final class StationMapView {
return 768.0;
}
+ double minimumHeight = resolveMinimumStationMapHeight(
+ chatPreferences.isGUIstationMapPathAnalysisVisible());
+
// Avoid restoring very large old test sizes after the layout changed.
- if (storedSize[1] < 650.0 || storedSize[1] > 1100.0) {
+ if (storedSize[1] < minimumHeight || storedSize[1] > 1100.0) {
return 768.0;
}