Fixed beacon implementation, pstrotator settings, Variable handling, Shortcut and snipped preferences ui, beacon handling, AS Settings and all fitting manual texts

This commit is contained in:
Marc Froehlich
2026-07-26 09:49:50 +02:00
parent 00496b56e9
commit ee974bbd73
12 changed files with 1092 additions and 572 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

+2 -1
View File
@@ -228,8 +228,9 @@ So kann der Contest-Operator auf einem Blick sehen, welche Stationen wann und ü
## Intervall-Beacon
Automatische CQ-Meldungen im öffentlichen Kanal in konfigurierbarem Intervall. Empfohlene Verwendung mit der Variable `MYQRG` für aktuelle Frequenzangabe. Details: [Konfiguration Beacon Settings](Konfiguration#beacon-settings-automatischer-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
+37 -2
View File
@@ -183,10 +183,45 @@ Wenn in der Benutzerliste ein Rufzeichen ausgewählt ist, wird der Snippet als D
## Beacon Settings (Automatischer Beacon)
Konfiguration eines automatischen Intervall-Beacons im öffentlichen Chat-Kanal. Empfohlen: Variable `MYQRG` im Text verwenden, damit die aktuelle Frequenz immer aktuell ist. Intervall und Text sind frei konfigurierbar.
![Beacon-Einstellungen](client_settings_window_beacon.png)
> **Tipp**: Beacon beim CQ-Rufen aktivieren und im Einstellungsfenster schnell deaktivieren, wenn kein CQ gerufen wird.
Ein Beacon sendet in regelmäßigen Abständen eine öffentliche CQ-Nachricht. Er ist für Betriebssituationen gedacht, in denen die eigene Station über längere Zeit auf einer festen Frequenz ruft. Andere Stationen erhalten dadurch eine aktuelle QRG-Information, ohne dass der Operator denselben Text wiederholt von Hand in den Chat schreiben muss.
KST4Contest verwendet einen gemeinsamen Timer für beide Chat-Kategorien. Aktivierung und Nachrichtentext werden trotzdem getrennt konfiguriert:
- **Enable CQ beacon** aktiviert den Beacon der betreffenden Kategorie.
- **Beacon message** enthält den öffentlichen Nachrichtentext dieser Kategorie.
- **Shared beacon interval** legt das gemeinsame Intervall für beide Kategorien fest.
Sind beide Beacons aktiviert, werden sie beim selben Timer-Lauf nacheinander in ihren jeweiligen Kategorien gesendet. Der zweite Beacon wird nur berücksichtigt, wenn auch der zweite Chat aktiviert und verbunden ist.
### Intervall und Timer-Verhalten
Das Intervall wird in ganzen Minuten angegeben. Der kleinste zulässige Wert ist eine Minute.
Nach dem Aufbau der Chat-Verbindung prüft KST4Contest die Beacons erstmals nach ungefähr zehn Sekunden. Anschließend gilt das eingestellte Intervall. Wird der Wert während einer laufenden Verbindung geändert, beginnt der Countdown mit dem neuen Intervall erneut. Die Änderung selbst löst keine sofortige Nachricht aus.
### Nachrichtentext und Variablen
Ein Beacon darf nach der Variablenauflösung höchstens 120 Zeichen enthalten. KST4Contest prüft deshalb nicht nur das eingetragene Template, sondern den tatsächlich zu sendenden Text.
Im Beacon können alle [globalen Variablen](de-Makros-und-Variablen#variablen-im-beacon) verwendet werden, beispielsweise:
```text
calling cq at MYQRG, ant MYQTF deg, loc MYLOCATOR
```
Die Variablen werden bei jedem Timer-Lauf neu aufgelöst. Ändert die Logsoftware zwischenzeitlich die in `MYQRG` gespeicherte Frequenz, verwendet bereits der nächste Beacon den neuen Wert.
Stationsbezogene Variablen wie `QRZNAME`, `FIRSTAP` oder `SECONDAP` benötigen dagegen eine ausgewählte Gegenstation. Da ein öffentlicher Beacon keine Gegenstation adressiert, werden diese Variablen im Beacon nicht aufgelöst.
### Wann sollte der Beacon ausgeschaltet werden?
Der Beacon ist nur dann hilfreich, wenn seine QRG-Angabe zum tatsächlichen Betrieb passt. Bleibt er beim Absuchen oder häufigen Wechseln von Frequenzen aktiviert, können andere Stationen auf einer inzwischen falschen Frequenz nach der eigenen Station suchen.
Im Klartext: Solange auf einer festen QRG CQ gerufen wird, spart der Beacon Arbeit. Beim „Schleichen“ über das Band sollte er ausgeschaltet werden.
Änderungen wirken während der laufenden Verbindung. Damit Aktivierung, Texte und Intervall auch nach dem nächsten Programmstart erhalten bleiben, anschließend **Save Settings** verwenden.
---
## Messagehandling Settings (ab v1.25)
+19 -3
View File
@@ -138,18 +138,34 @@ Wird durch die aktuelle Antennenrichtung in Worten ersetzt (z. B. `north`, `nort
---
## Variablen im Beacon
## Variablen im Beacon
Alle Variablen können auch im **automatischen Beacon** (Intervall-Nachrichten) verwendet werden. Empfohlene Beacon-Konfiguration:
Ein öffentlicher Beacon besitzt keine ausgewählte Gegenstation. Deshalb können hier ausschließlich Variablen verwendet werden, die nur von der eigenen Station und ihrer aktuellen Konfiguration abhängen:
| Variable | Wert im Beacon |
|---|---|
| `MYQRG` | aktuelle QRG der ersten Chat-Kategorie |
| `MYQRGSHORT` | auf sieben Zeichen gekürzte QRG der ersten Kategorie |
| `SECONDQRG` | aktuelle QRG der zweiten Chat-Kategorie |
| `MYLOCATOR` | eigener vollständiger Locator |
| `MYLOCATORSHORT` | eigener vierstelliger Locator |
| `MYCALL` | eigenes Rufzeichen |
| `MYQTF` | aktuelle Antennenrichtung |
`QRZNAME`, `FIRSTAP` und `SECONDAP` benötigen eine ausgewählte Station. In einem öffentlichen Beacon werden sie daher nicht aufgelöst.
Eine zweckmäßige Konfiguration ist beispielsweise:
```
calling cq at MYQRG, loc MYLOCATOR, GL all!
```
Da KST4Contest QRG-Daten automatisch aus Chat-Nachrichten ausliest: Wenn andere Stationen ebenfalls KST4Contest nutzen, sehen sie die eigene QRG sofort in der QRG-Spalte der Benutzerliste.
Die globalen Variablen werden bei jedem Timer-Lauf neu ausgewertet. Dadurch kann eine vom Logprogramm aktualisierte QRG bereits in der nächsten Beacon-Nachricht erscheinen.
Der vollständig aufgelöste Nachrichtentext darf höchstens 120 Zeichen enthalten. Weitere Angaben zum gemeinsamen Intervall und zum Verhalten beider Chat-Kategorien stehen unter [Konfiguration Beacon Settings](de-Konfiguration#beacon-settings-automatischer-beacon).
---
## Beispiel-Workflow mit Makros im Contest
1. Station in der Benutzerliste auswählen → Rufzeichen ist nun vorausgewählt.
+12
View File
@@ -150,6 +150,18 @@
<version>${jetbrains.annotations.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.12.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.17.0</version>
<scope>compile</scope>
</dependency>
<!--
<dependency>
@@ -1,122 +1,178 @@
package kst4contest.controller;
import java.util.Arrays;
import java.util.TimerTask;
import kst4contest.model.ChatMessage;
import kst4contest.model.ThreadStateMessage;
/**
* This class is for sending beacons intervalled to the public chat. Gets all
* preferences and instances via the Chatpreferences-object of the
* Chatcontroller.
* <br/><br/>
* The task will be runned out of the singleton ChatController instance in an
* intervall as specified by the Chatpreferences-instance (typically as
* configured in the xml file.
*
*
* @author prakt
* Sends the configured public-chat beacons for both active chat categories.
*
* <p>Both categories deliberately share one timer and therefore the same
* interval. Their enable flags and message templates remain independent. Every
* run reads the current preferences, resolves global message variables and
* sends only the categories which are currently enabled.</p>
*/
public class BeaconTask extends TimerTask {
private ChatController chatController;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "MyBeacon";
private static final String THREAD_NICKNAME = "MyBeacon";
public BeaconTask(ChatController client, ThreadStatusCallback callback) {
this.callBackToController = callback;
this.chatController = client;
private final ChatController chatController;
private final ThreadStatusCallback callbackToController;
/**
* Creates one execution of the shared beacon timer.
*
* @param chatController controller providing preferences and the TX queue
* @param callbackToController callback used by the thread-status display
*/
public BeaconTask(
ChatController chatController,
ThreadStatusCallback callbackToController
) {
this.chatController = chatController;
this.callbackToController = callbackToController;
}
@Override
public void run() {
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
Thread.currentThread().setName("BeaconTask");
reportStatus(THREAD_NICKNAME, true, "initialized", false);
MessageVariableResolver variableResolver =
new MessageVariableResolver(chatController.getChatPreferences());
ChatMessage beaconMSG = new ChatMessage();
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat();
replaceVariables = replaceVariables.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
replaceVariables = replaceVariables.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
replaceVariables = replaceVariables.replaceAll("SECONDQRG", this.chatController.getChatPreferences().getMYQRGSecondCat().getValue() + "");
beaconMSG.setMessageText(
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
beaconMSG.setMessageDirectedToServer(true);
ChatMessage beaconMSG2 = new ChatMessage();
String replaceVariables2 = this.chatController.getChatPreferences().getBcn_beaconTextSecondCat();
replaceVariables2 = replaceVariables2.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
replaceVariables2 = replaceVariables2.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
replaceVariables2 = replaceVariables2.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
replaceVariables2 = replaceVariables2.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
replaceVariables2 = replaceVariables2.replaceAll("SECONDQRG", this.chatController.getChatPreferences().getMYQRGSecondCat().getValue() + "");
beaconMSG2.setMessageText(
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategorySecond().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
beaconMSG2.setMessageDirectedToServer(true);
/**
* beacon 1st Chatcategory
*/
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledMainCat() ) {
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
+ " [BeaconTask, Info]: Sending CQ: " + beaconMSG.getMessageText());
this.chatController.getMessageTXBus().add(beaconMSG);
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", true, "on", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} else {
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", false, "off", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
sendMainCategoryBeacon(variableResolver);
sendSecondCategoryBeacon(variableResolver);
}
/**
* beacon 2nd Chatcategory
* Sends the main-category beacon if it is currently enabled.
*/
if (this.chatController.getChatPreferences().isLoginToSecondChatEnabled()) { //only send if 2nd cat enabled
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledSecondCat()) {
beaconMSG2.setMessageText(
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategorySecond().getCategoryNumber() + "|0|" + replaceVariables2 + "|0|");
beaconMSG2.setMessageDirectedToServer(true);
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
+ " [BeaconTask, Info]: Sending CQ 2nd Cat: " + beaconMSG2.getMessageText());
this.chatController.getMessageTXBus().add(beaconMSG2);
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", true, "on", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} else {
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", false, "off", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
private void sendMainCategoryBeacon(MessageVariableResolver variableResolver) {
if (!chatController.getChatPreferences().isBcn_beaconsEnabledMainCat()) {
reportStatus(THREAD_NICKNAME + " 1", false, "off", false);
return;
}
String resolvedText = variableResolver.resolveGlobalVariables(
chatController.getChatPreferences().getBcn_beaconTextMainCat()
);
ChatMessage beaconMessage = buildBeaconMessage(
chatController.getChatPreferences()
.getLoginChatCategoryMain()
.getCategoryNumber(),
resolvedText,
"main category"
);
if (beaconMessage == null) {
reportStatus(THREAD_NICKNAME + " 1", false, "invalid text", true);
return;
}
System.out.println(
new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
+ " [BeaconTask, Info]: Sending main-category CQ: "
+ beaconMessage.getMessageText()
);
chatController.getMessageTXBus().add(beaconMessage);
reportStatus(THREAD_NICKNAME + " 1", true, "on", false);
}
/**
* Sends the second-category beacon if the second login and its beacon are
* currently enabled.
*/
private void sendSecondCategoryBeacon(
MessageVariableResolver variableResolver
) {
if (!chatController.getChatPreferences().isLoginToSecondChatEnabled()
|| !chatController.getChatPreferences()
.isBcn_beaconsEnabledSecondCat()) {
reportStatus(THREAD_NICKNAME + " 2", false, "off", false);
return;
}
String resolvedText = variableResolver.resolveGlobalVariables(
chatController.getChatPreferences().getBcn_beaconTextSecondCat()
);
ChatMessage beaconMessage = buildBeaconMessage(
chatController.getChatPreferences()
.getLoginChatCategorySecond()
.getCategoryNumber(),
resolvedText,
"second category"
);
if (beaconMessage == null) {
reportStatus(THREAD_NICKNAME + " 2", false, "invalid text", true);
return;
}
System.out.println(
new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
+ " [BeaconTask, Info]: Sending second-category CQ: "
+ beaconMessage.getMessageText()
);
chatController.getMessageTXBus().add(beaconMessage);
reportStatus(THREAD_NICKNAME + " 2", true, "on", false);
}
/**
* Builds the server-directed message after validating the resolved payload.
*
* <p>The resolved text is checked rather than only the configured template
* because inserted values can increase the final message length.</p>
*
* @param categoryNumber ON4KST category number
* @param resolvedText fully resolved beacon payload
* @param categoryDescription text used in diagnostic output
* @return prepared message, or {@code null} if the payload is invalid
*/
private ChatMessage buildBeaconMessage(
int categoryNumber,
String resolvedText,
String categoryDescription
) {
if (resolvedText == null
|| resolvedText.length() > ChatController.MAX_BEACON_TEXT_LENGTH) {
int actualLength = resolvedText == null ? 0 : resolvedText.length();
System.out.println(
"[BeaconTask, Warning]: Beacon for "
+ categoryDescription
+ " was not sent because the resolved text contains "
+ actualLength
+ " characters; maximum is "
+ ChatController.MAX_BEACON_TEXT_LENGTH
+ "."
);
return null;
}
ChatMessage beaconMessage = new ChatMessage();
beaconMessage.setMessageText(
"MSG|" + categoryNumber + "|0|" + resolvedText + "|0|"
);
beaconMessage.setMessageDirectedToServer(true);
return beaconMessage;
}
/**
* Forwards one state update to the existing thread-status display.
*/
private void reportStatus(
String threadName,
boolean running,
String information,
boolean criticalState
) {
ThreadStateMessage stateMessage = new ThreadStateMessage(
threadName,
running,
information,
criticalState
);
callbackToController.onThreadStatus(THREAD_NICKNAME, stateMessage);
}
}
@@ -59,6 +59,10 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
private static final boolean DEBUG_BAND_UPGRADE_HINT = true; //for new band hint
public static final int MIN_BEACON_INTERVAL_MINUTES = 1;
public static final int MAX_BEACON_TEXT_LENGTH = 120;
private static final long INITIAL_BEACON_DELAY_MILLIS = 10_000L;
private PstRotatorClient rotatorClient;
private Consumer<Double> viewRotorCallback;
@@ -708,8 +712,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
// writeThread.interrupt();
// readThread.interrupt();
beaconTimer.purge();
beaconTimer.cancel();
stopBeaconTimer();
ASQueryTimer.purge();
ASQueryTimer.cancel();
socketCheckTimer.purge();
@@ -2239,6 +2242,78 @@ private ObservableList<String>
this.dbHandler = dbHandler;
}
/**
* Starts the shared beacon timer with the interval currently stored in the
* preferences.
*
* <p>Both chat categories deliberately use the same timer. The task checks the
* individual enable flag and text for each category every time it runs.</p>
*
* @param initialDelayMillis delay before the next beacon run
*/
private synchronized void scheduleBeaconTimer(long initialDelayMillis) {
stopBeaconTimer();
int configuredInterval =
chatPreferences.getBcn_beaconIntervalInMinutesMainCat();
int effectiveInterval =
Math.max(MIN_BEACON_INTERVAL_MINUTES, configuredInterval);
/*
* Keep both legacy XML values synchronized. The second value remains in the
* configuration for backward compatibility, but no longer represents an
* independent timer.
*/
chatPreferences.setBcn_beaconIntervalInMinutesMainCat(effectiveInterval);
chatPreferences.setBcn_beaconIntervalInMinutesSecondCat(effectiveInterval);
long intervalMillis = TimeUnit.MINUTES.toMillis(effectiveInterval);
long safeInitialDelay = Math.max(0L, initialDelayMillis);
beaconTimer = new Timer("BeaconTimer");
beaconTimer.schedule(
new BeaconTask(this, this),
safeInitialDelay,
intervalMillis
);
System.out.println("[ChatController, Info]: Shared beacon timer scheduled every "
+ effectiveInterval + " minute(s).");
}
/**
* Applies a changed beacon interval while the chat connection is running.
*
* <p>The countdown starts again with the new interval. No beacon is sent
* immediately merely because the setting was changed.</p>
*/
public synchronized void restartBeaconTimer() {
if (!isConnectedAndLoggedIn()) {
return;
}
long intervalMillis = TimeUnit.MINUTES.toMillis(
Math.max(
MIN_BEACON_INTERVAL_MINUTES,
chatPreferences.getBcn_beaconIntervalInMinutesMainCat()
)
);
scheduleBeaconTimer(intervalMillis);
}
/**
* Stops the shared beacon timer if it is currently running.
*/
private synchronized void stopBeaconTimer() {
if (beaconTimer == null) {
return;
}
beaconTimer.cancel();
beaconTimer.purge();
beaconTimer = null;
}
/**
* execute is the main entry point where the application starts.
* @throws InterruptedException
@@ -2338,11 +2413,7 @@ private ObservableList<String>
* The CQ-beacon-Task will be executed every time but checks for itself whether
* CQ messages are enabled or not
*/
// Timer beaconTimer;
beaconTimer = new Timer();
beaconTimer.schedule(new BeaconTask(this, this), 10000,
this.getChatPreferences().getBcn_beaconIntervalInMinutesMainCat() * 60000);
// 60000 * intervalInMinutes = IntervalInMillis
scheduleBeaconTimer(INITIAL_BEACON_DELAY_MILLIS);
/**
* The AS querier task will be executed every time but checks for itself whether
@@ -0,0 +1,154 @@
package kst4contest.controller;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import kst4contest.model.AirPlane;
import kst4contest.model.AirPlaneReflectionInfo;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
/**
* Resolves variables used in operator messages, shortcuts, snippets and
* beacons.
*
* <p>Global variables only depend on the local station configuration and can
* therefore be used in every message context. Station variables additionally
* require a selected remote station. Keeping both groups in one resolver
* prevents the send field and the beacon tasks from implementing different
* replacement rules.</p>
*/
public final class MessageVariableResolver {
private static final int SHORT_VALUE_LENGTH = 7;
private final ChatPreferences chatPreferences;
/**
* Creates a resolver backed by the live application preferences.
*
* @param chatPreferences preferences that provide current station values
*/
public MessageVariableResolver(ChatPreferences chatPreferences) {
this.chatPreferences = Objects.requireNonNull(chatPreferences, "chatPreferences");
}
/**
* Resolves variables which do not require a selected remote station.
*
* <p>The replacement is literal rather than regular-expression based.
* Callsigns, locators and frequencies are data, not regular expressions.</p>
*
* @param template text that may contain variables
* @return text with all available global variables resolved, or {@code null}
* when the supplied template is {@code null}
*/
public String resolveGlobalVariables(String template) {
if (template == null) {
return null;
}
String primaryFrequency = valueOrEmpty(chatPreferences.getMYQRGFirstCat().getValue());
String secondaryFrequency = valueOrEmpty(chatPreferences.getMYQRGSecondCat().getValue());
String ownLocator = valueOrEmpty(chatPreferences.getStn_loginLocatorMainCat());
String resolvedText = template;
resolvedText = resolvedText.replace("MYQRGSHORT", abbreviate(primaryFrequency));
resolvedText = resolvedText.replace("MYQRG", primaryFrequency);
resolvedText = resolvedText.replace("SECONDQRG", secondaryFrequency);
resolvedText = resolvedText.replace("MYLOCATORSHORT", abbreviateLocator(ownLocator));
resolvedText = resolvedText.replace("MYLOCATOR", ownLocator);
resolvedText = resolvedText.replace("MYCALL", valueOrEmpty(chatPreferences.getStn_loginCallSign()));
resolvedText = resolvedText.replace("MYQTF", formatHeading(chatPreferences.getActualQTF().getValue().doubleValue()));
return resolvedText;
}
/**
* Resolves global variables and variables derived from a selected station.
*
* <p>If no station is selected, station-specific placeholders remain visible.
* This is intentional: silently removing {@code QRZNAME}, {@code FIRSTAP} or
* {@code SECONDAP} could create a plausible-looking but incomplete message.</p>
*
* @param template text that may contain variables
* @param selectedStation currently selected remote station, may be {@code null}
* @return resolved message text
*/
public String resolveForSelectedStation(String template, ChatMember selectedStation) {
String resolvedText = resolveGlobalVariables(template);
if (resolvedText == null || selectedStation == null) {
return resolvedText;
}
resolvedText = resolvedText.replace("QRZNAME", resolveStationName(selectedStation));
resolvedText = resolvedText.replace("FIRSTAP", resolveFirstAirPlane(selectedStation));
resolvedText = resolvedText.replace("SECONDAP", resolveSecondAirPlane(selectedStation));
return resolvedText;
}
private String resolveStationName(ChatMember selectedStation) {
String stationName = valueOrEmpty(selectedStation.getName()).trim();
if (!stationName.isEmpty()) {
return stationName;
}
return valueOrEmpty(selectedStation.getCallSign());
}
private String resolveFirstAirPlane(ChatMember selectedStation) {
List<AirPlane> risingAirPlanes = getRisingAirPlanes(selectedStation);
if (risingAirPlanes.isEmpty()) {
return "no ap available";
}
AirPlane firstAirPlane = risingAirPlanes.get(0);
return "a " + firstAirPlane.getPotencialDescriptionAsWord()
+ " in " + firstAirPlane.getArrivingDurationMinutes() + " min";
}
private String resolveSecondAirPlane(ChatMember selectedStation) {
List<AirPlane> risingAirPlanes = getRisingAirPlanes(selectedStation);
if (risingAirPlanes.size() < 2) {
return "";
}
AirPlane secondAirPlane = risingAirPlanes.get(1);
return "Next " + secondAirPlane.getPotencialDescriptionAsWord()
+ " in " + secondAirPlane.getArrivingDurationMinutes() + " min";
}
private List<AirPlane> getRisingAirPlanes(ChatMember selectedStation) {
AirPlaneReflectionInfo reflectionInfo = selectedStation.getAirPlaneReflectInfo();
if (reflectionInfo == null || reflectionInfo.getRisingAirplanes() == null) {
return List.of();
}
return reflectionInfo.getRisingAirplanes();
}
private String abbreviate(String value) {
return value.substring(0, Math.min(value.length(), SHORT_VALUE_LENGTH));
}
private String abbreviateLocator(String locator) {
return locator.substring(0, Math.min(locator.length(), 4));
}
private String formatHeading(double headingDegrees) {
if (!Double.isFinite(headingDegrees)) {
return "";
}
return BigDecimal.valueOf(headingDegrees).stripTrailingZeros().toPlainString();
}
private String valueOrEmpty(String value) {
return value == null ? "" : value;
}
}
@@ -35,13 +35,11 @@ public class ScoreboardUpdateTask extends TimerTask {
ChatMessage beaconMSG = new ChatMessage();
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat();
// replaceVariables = bcn_beaconText;
replaceVariables = replaceVariables.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
replaceVariables = replaceVariables.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
MessageVariableResolver variableResolver =
new MessageVariableResolver(this.chatController.getChatPreferences());
String replaceVariables = variableResolver.resolveGlobalVariables(
this.chatController.getChatPreferences().getBcn_beaconTextMainCat()
);
beaconMSG.setMessageText(
@@ -0,0 +1,112 @@
package kst4contest.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import kst4contest.controller.MessageVariableResolver;
import kst4contest.model.AirPlane;
import kst4contest.model.AirPlaneReflectionInfo;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
class MessageVariableResolverTest {
private ChatPreferences chatPreferences;
private MessageVariableResolver resolver;
@BeforeEach
void setUp() {
chatPreferences = mock(ChatPreferences.class);
when(chatPreferences.getMYQRGFirstCat()).thenReturn(new SimpleStringProperty("144.388.03"));
when(chatPreferences.getMYQRGSecondCat()).thenReturn(new SimpleStringProperty("432.088.00"));
when(chatPreferences.getStn_loginLocatorMainCat()).thenReturn("JO51IJ");
when(chatPreferences.getStn_loginCallSign()).thenReturn("DO5AMF");
when(chatPreferences.getActualQTF()).thenReturn(new SimpleDoubleProperty(135.0));
resolver = new MessageVariableResolver(chatPreferences);
}
@Test
void resolvesAllGlobalVariablesInEveryMessageContext() {
String template = "MYCALL MYQRGSHORT MYQRG SECONDQRG MYLOCATORSHORT MYLOCATOR MYQTF";
assertEquals(
"DO5AMF 144.388 144.388.03 432.088.00 JO51 JO51IJ 135",
resolver.resolveGlobalVariables(template)
);
}
@Test
void shortValuesRemainSafeWhenTheConfiguredValueIsShorterThanExpected() {
when(chatPreferences.getMYQRGFirstCat()).thenReturn(new SimpleStringProperty("144"));
when(chatPreferences.getStn_loginLocatorMainCat()).thenReturn("JO5");
assertEquals("144 JO5", resolver.resolveGlobalVariables("MYQRGSHORT MYLOCATORSHORT"));
}
@Test
void resolvesSelectedStationNameAndTheFirstTwoAirPlanes() {
ChatMember selectedStation = new ChatMember();
selectedStation.setCallSign("DL0TEST");
selectedStation.setName("Test Operator");
AirPlane firstAirPlane = new AirPlane();
firstAirPlane.setPotential(100);
firstAirPlane.setArrivingDurationMinutes(1);
AirPlane secondAirPlane = new AirPlane();
secondAirPlane.setPotential(75);
secondAirPlane.setArrivingDurationMinutes(9);
AirPlaneReflectionInfo reflectionInfo = new AirPlaneReflectionInfo();
reflectionInfo.setRisingAirplanes(FXCollections.observableArrayList(firstAirPlane, secondAirPlane));
selectedStation.setAirPlaneReflectInfo(reflectionInfo);
assertEquals(
"Hi Test Operator, a very big AP in 1 min; Next big AP in 9 min",
resolver.resolveForSelectedStation(
"Hi QRZNAME, FIRSTAP; SECONDAP",
selectedStation
)
);
}
@Test
void usesCallsignWhenTheSelectedStationHasNoName() {
ChatMember selectedStation = new ChatMember();
selectedStation.setCallSign("DL0TEST");
selectedStation.setName(" ");
assertEquals(
"Hi DL0TEST",
resolver.resolveForSelectedStation("Hi QRZNAME", selectedStation)
);
}
@Test
void keepsStationVariablesVisibleWhenNoStationIsSelected() {
assertEquals(
"QRZNAME FIRSTAP SECONDAP",
resolver.resolveForSelectedStation("QRZNAME FIRSTAP SECONDAP", null)
);
}
@Test
void returnsUsefulFallbacksWhenNoAirPlaneIsAvailable() {
ChatMember selectedStation = new ChatMember();
selectedStation.setCallSign("DL0TEST");
assertEquals(
"no ap available ",
resolver.resolveForSelectedStation("FIRSTAP SECONDAP", selectedStation)
);
}
}
File diff suppressed because it is too large Load Diff
+2
View File
@@ -10,6 +10,8 @@ module praktiKST {
requires java.net.http;
requires java.desktop;
requires jdk.crypto.ec;
requires org.junit.jupiter.api;
requires org.mockito;
exports kst4contest.controller.interfaces;
exports kst4contest.controller;
exports kst4contest.locatorUtils;