mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-18 21:34:03 +02:00
fix: suppress automatic DX cluster spots for worked stations (#91)
Automatic spots from directional opportunities were generated without looking at the worked state, so stations already in the log kept being spotted to the connected loggers. The spot block in MessageBusManagementThread now asks isAlreadyWorkedForDxClusterSpot(sender) before broadcasting. The rule: - the band is taken from the sender's known frequency via FrequencyTextParser; an unknown band keeps the previous behaviour; - the per-band worked flag of that band suppresses the spot (ChatMember.isWorkedOnBand maps B_1296/B_2320/B_5760 to the historically named worked1240/worked2300/worked5600 flags); - the global worked flag suppresses the spot only when no per-band flag exists at all (Simplelogfile source without band information); - the global flag together with per-band flags for other bands does not suppress, because the per-band information is authoritative. Suppressed spots are logged at FINE. The direction check, the inAngleAndRange marker, the tick sound and manual spots from the map window and the settings test spot are unchanged. Fixes #91 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
ab28216280
commit
78de8b0a9c
4 files changed
+410
-1
No files matched your search
@@ -1310,7 +1310,8 @@ public class MessageBusManagementThread extends Thread {
|
||||
? null
|
||||
: sender.getFrequency().get();
|
||||
|
||||
if (detectedFrequency != null && !detectedFrequency.isBlank()) {
|
||||
if (detectedFrequency != null && !detectedFrequency.isBlank()
|
||||
&& !isAlreadyWorkedForDxClusterSpot(sender)) {
|
||||
/*
|
||||
* The DX Cluster spot must not depend on AirScout data. A known
|
||||
* frequency and a valid directional opportunity are sufficient.
|
||||
@@ -1329,6 +1330,10 @@ public class MessageBusManagementThread extends Thread {
|
||||
onlyForSpottingObject
|
||||
);
|
||||
|
||||
} else if (detectedFrequency != null && !detectedFrequency.isBlank()) {
|
||||
// Only reached when the worked check was the reason (issue #91).
|
||||
LOGGER.fine("DX Cluster spot suppressed, station already worked on this band: "
|
||||
+ sender.getCallSign());
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
LOGGER.log(Level.WARNING,
|
||||
@@ -1852,6 +1857,51 @@ public class MessageBusManagementThread extends Thread {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decides whether an automatic DX Cluster spot must be suppressed because
|
||||
* the sender has already been worked on the band of its known frequency.
|
||||
*
|
||||
* <p>Rules (issue #91):</p>
|
||||
* <ul>
|
||||
* <li>No sender, no frequency or a frequency outside the supported
|
||||
* bands: the existing behaviour is kept and the spot is allowed.</li>
|
||||
* <li>The per-band worked flag of the detected band is set: suppress.</li>
|
||||
* <li>Only the global worked flag is set and no per-band flag exists
|
||||
* (Simplelogfile source without band information): suppress, because
|
||||
* the band cannot be told apart.</li>
|
||||
* <li>The global flag is set together with per-band flags for other
|
||||
* bands: allow, because the per-band information is authoritative.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Manual spots from the map window and the settings test spot do not
|
||||
* use this check.</p>
|
||||
*
|
||||
* @param sender station that triggered the directional opportunity
|
||||
* @return {@code true} when the automatic spot has to be suppressed
|
||||
*/
|
||||
static boolean isAlreadyWorkedForDxClusterSpot(ChatMember sender) {
|
||||
if (sender == null || sender.getFrequency() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FrequencyTextParser.DetectedFrequency detectedFrequency =
|
||||
FrequencyTextParser.parseExplicitFrequency(
|
||||
sender.getFrequency().get()
|
||||
);
|
||||
|
||||
if (detectedFrequency == null) {
|
||||
// Unknown band: keep the existing behaviour and allow the spot.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sender.isWorkedOnBand(detectedFrequency.getBand())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Worked source without band information (Simplelogfile): global flag only.
|
||||
return sender.isWorked() && !sender.hasAnyBandWorkedFlag();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the comment transmitted with a local DX Cluster spot.
|
||||
*
|
||||
|
||||
@@ -529,6 +529,76 @@ public class ChatMember {
|
||||
return worked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the per-band worked flag that belongs to the given band.
|
||||
*
|
||||
* <p>The flag names historically differ from the {@link Band} names:
|
||||
* {@code B_1296} maps to {@link #isWorked1240()}, {@code B_2320} to
|
||||
* {@link #isWorked2300()} and {@code B_5760} to {@link #isWorked5600()}.
|
||||
* This is the same mapping that the worked gross-field cache and the
|
||||
* external-log band mapping use. The global {@link #isWorked()} flag is
|
||||
* deliberately not consulted here.</p>
|
||||
*
|
||||
* @param band band to check, may be {@code null}
|
||||
* @return {@code true} when the station is marked worked on that band;
|
||||
* {@code false} for {@code null} or a band without a flag
|
||||
*/
|
||||
public boolean isWorkedOnBand(Band band) {
|
||||
if (band == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (band) {
|
||||
case B_50:
|
||||
return isWorked50();
|
||||
case B_70:
|
||||
return isWorked70();
|
||||
case B_144:
|
||||
return isWorked144();
|
||||
case B_432:
|
||||
return isWorked432();
|
||||
case B_1296:
|
||||
return isWorked1240();
|
||||
case B_2320:
|
||||
return isWorked2300();
|
||||
case B_3400:
|
||||
return isWorked3400();
|
||||
case B_5760:
|
||||
return isWorked5600();
|
||||
case B_10G:
|
||||
return isWorked10G();
|
||||
case B_24G:
|
||||
return isWorked24G();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells whether any per-band worked flag is set.
|
||||
*
|
||||
* <p>Sources without band information (for example the Simplelogfile
|
||||
* interpreter) only set the global {@link #isWorked()} flag. A caller can
|
||||
* use this method to distinguish such a station from one whose per-band
|
||||
* state is known. The global flag itself is not part of the result.</p>
|
||||
*
|
||||
* @return {@code true} when at least one per-band flag is set
|
||||
*/
|
||||
public boolean hasAnyBandWorkedFlag() {
|
||||
return isWorked50()
|
||||
|| isWorked70()
|
||||
|| isWorked144()
|
||||
|| isWorked432()
|
||||
|| isWorked1240()
|
||||
|| isWorked2300()
|
||||
|| isWorked3400()
|
||||
|| isWorked5600()
|
||||
|| isWorked10G()
|
||||
|| isWorked24G()
|
||||
|| isWorked47G()
|
||||
|| isWorked76G();
|
||||
}
|
||||
|
||||
public boolean isWorked50() {
|
||||
return Worked50;
|
||||
}
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.observe.SimpleValue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Covers the worked-state check that suppresses automatic DX Cluster spots
|
||||
* (issue #91). Manual spots do not use this predicate.
|
||||
*/
|
||||
class MessageBusManagementThreadDxClusterWorkedTest {
|
||||
|
||||
@Test
|
||||
void suppressesSpotWhenWorkedOnDetectedBand() {
|
||||
ChatMember sender = senderOn("144.3");
|
||||
sender.setWorked144(true);
|
||||
|
||||
assertTrue(MessageBusManagementThread.isAlreadyWorkedForDxClusterSpot(sender));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsSpotWhenWorkedOnlyOnAnotherBand() {
|
||||
ChatMember sender = senderOn("432.2");
|
||||
sender.setWorked144(true);
|
||||
|
||||
assertFalse(MessageBusManagementThread.isAlreadyWorkedForDxClusterSpot(sender));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapsBand1296ToWorked1240Flag() {
|
||||
ChatMember sender = senderOn("1296.2");
|
||||
sender.setWorked1240(true);
|
||||
|
||||
assertTrue(MessageBusManagementThread.isAlreadyWorkedForDxClusterSpot(sender));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsSpotWhenNothingIsWorked() {
|
||||
ChatMember sender = senderOn("144.3");
|
||||
|
||||
assertFalse(MessageBusManagementThread.isAlreadyWorkedForDxClusterSpot(sender));
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalWorkedFlagWithoutBandInformationSuppressesSpot() {
|
||||
// Simplelogfile source: only the global flag is known, no per-band flags.
|
||||
ChatMember sender = senderOn("144.3");
|
||||
sender.setWorked(true);
|
||||
|
||||
assertTrue(MessageBusManagementThread.isAlreadyWorkedForDxClusterSpot(sender));
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalWorkedFlagWithOtherBandInformationAllowsSpot() {
|
||||
// Per-band information exists and says 432 only, so 144 is still open.
|
||||
ChatMember sender = senderOn("144.3");
|
||||
sender.setWorked(true);
|
||||
sender.setWorked432(true);
|
||||
|
||||
assertFalse(MessageBusManagementThread.isAlreadyWorkedForDxClusterSpot(sender));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsSpotWhenFrequencyWrapperIsNull() {
|
||||
ChatMember sender = new ChatMember();
|
||||
sender.setFrequency(null);
|
||||
sender.setWorked(true);
|
||||
sender.setWorked144(true);
|
||||
|
||||
assertFalse(MessageBusManagementThread.isAlreadyWorkedForDxClusterSpot(sender));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsSpotWhenFrequencyValueIsNull() {
|
||||
ChatMember sender = new ChatMember();
|
||||
sender.setFrequency(new SimpleValue<>());
|
||||
sender.setWorked(true);
|
||||
sender.setWorked144(true);
|
||||
|
||||
assertFalse(MessageBusManagementThread.isAlreadyWorkedForDxClusterSpot(sender));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsSpotWhenFrequencyIsEmpty() {
|
||||
ChatMember sender = senderOn("");
|
||||
sender.setWorked(true);
|
||||
sender.setWorked144(true);
|
||||
|
||||
assertFalse(MessageBusManagementThread.isAlreadyWorkedForDxClusterSpot(sender));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsSpotWhenFrequencyIsOutsideKnownBands() {
|
||||
ChatMember sender = senderOn("999.9");
|
||||
sender.setWorked(true);
|
||||
sender.setWorked144(true);
|
||||
|
||||
assertFalse(MessageBusManagementThread.isAlreadyWorkedForDxClusterSpot(sender));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsSpotWhenSenderIsNull() {
|
||||
assertFalse(MessageBusManagementThread.isAlreadyWorkedForDxClusterSpot(null));
|
||||
}
|
||||
|
||||
private static ChatMember senderOn(String frequencyMHz) {
|
||||
ChatMember sender = new ChatMember();
|
||||
sender.setCallSign("DL1ABC");
|
||||
sender.setFrequency(new SimpleValue<>(frequencyMHz));
|
||||
return sender;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package kst4contest.model;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
|
||||
class ChatMemberWorkedOnBandTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(Band.class)
|
||||
void noFlagSetMeansNotWorkedOnAnyBand(Band band) {
|
||||
ChatMember member = new ChatMember();
|
||||
|
||||
assertFalse(member.isWorkedOnBand(band));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullBandIsNeverWorked() {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setWorked144(true);
|
||||
|
||||
assertFalse(member.isWorkedOnBand(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalWorkedFlagDoesNotCountAsBandFlag() {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setWorked(true);
|
||||
|
||||
for (Band band : Band.values()) {
|
||||
assertFalse(member.isWorkedOnBand(band), band.name());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void worked50MapsToBand50() {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setWorked50(true);
|
||||
|
||||
assertOnlyBandWorked(member, Band.B_50);
|
||||
}
|
||||
|
||||
@Test
|
||||
void worked70MapsToBand70() {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setWorked70(true);
|
||||
|
||||
assertOnlyBandWorked(member, Band.B_70);
|
||||
}
|
||||
|
||||
@Test
|
||||
void worked144MapsToBand144() {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setWorked144(true);
|
||||
|
||||
assertOnlyBandWorked(member, Band.B_144);
|
||||
}
|
||||
|
||||
@Test
|
||||
void worked432MapsToBand432() {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setWorked432(true);
|
||||
|
||||
assertOnlyBandWorked(member, Band.B_432);
|
||||
}
|
||||
|
||||
@Test
|
||||
void worked1240MapsToBand1296() {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setWorked1240(true);
|
||||
|
||||
assertOnlyBandWorked(member, Band.B_1296);
|
||||
}
|
||||
|
||||
@Test
|
||||
void worked2300MapsToBand2320() {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setWorked2300(true);
|
||||
|
||||
assertOnlyBandWorked(member, Band.B_2320);
|
||||
}
|
||||
|
||||
@Test
|
||||
void worked3400MapsToBand3400() {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setWorked3400(true);
|
||||
|
||||
assertOnlyBandWorked(member, Band.B_3400);
|
||||
}
|
||||
|
||||
@Test
|
||||
void worked5600MapsToBand5760() {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setWorked5600(true);
|
||||
|
||||
assertOnlyBandWorked(member, Band.B_5760);
|
||||
}
|
||||
|
||||
@Test
|
||||
void worked10GMapsToBand10G() {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setWorked10G(true);
|
||||
|
||||
assertOnlyBandWorked(member, Band.B_10G);
|
||||
}
|
||||
|
||||
@Test
|
||||
void worked24GMapsToBand24G() {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setWorked24G(true);
|
||||
|
||||
assertOnlyBandWorked(member, Band.B_24G);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasAnyBandWorkedFlagIsFalseWithoutFlags() {
|
||||
ChatMember member = new ChatMember();
|
||||
|
||||
assertFalse(member.hasAnyBandWorkedFlag());
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasAnyBandWorkedFlagIgnoresGlobalWorkedFlag() {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setWorked(true);
|
||||
|
||||
assertFalse(member.hasAnyBandWorkedFlag());
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasAnyBandWorkedFlagDetectsEachBandFlag() {
|
||||
assertTrue(memberWith(m -> m.setWorked50(true)).hasAnyBandWorkedFlag());
|
||||
assertTrue(memberWith(m -> m.setWorked70(true)).hasAnyBandWorkedFlag());
|
||||
assertTrue(memberWith(m -> m.setWorked144(true)).hasAnyBandWorkedFlag());
|
||||
assertTrue(memberWith(m -> m.setWorked432(true)).hasAnyBandWorkedFlag());
|
||||
assertTrue(memberWith(m -> m.setWorked1240(true)).hasAnyBandWorkedFlag());
|
||||
assertTrue(memberWith(m -> m.setWorked2300(true)).hasAnyBandWorkedFlag());
|
||||
assertTrue(memberWith(m -> m.setWorked3400(true)).hasAnyBandWorkedFlag());
|
||||
assertTrue(memberWith(m -> m.setWorked5600(true)).hasAnyBandWorkedFlag());
|
||||
assertTrue(memberWith(m -> m.setWorked10G(true)).hasAnyBandWorkedFlag());
|
||||
assertTrue(memberWith(m -> m.setWorked24G(true)).hasAnyBandWorkedFlag());
|
||||
assertTrue(memberWith(m -> m.setWorked47G(true)).hasAnyBandWorkedFlag());
|
||||
assertTrue(memberWith(m -> m.setWorked76G(true)).hasAnyBandWorkedFlag());
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasAnyBandWorkedFlagIsFalseAfterReset() {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setWorked144(true);
|
||||
member.setWorked76G(true);
|
||||
member.resetWorkedInformationAtAllBands();
|
||||
|
||||
assertFalse(member.hasAnyBandWorkedFlag());
|
||||
}
|
||||
|
||||
private static void assertOnlyBandWorked(ChatMember member, Band expected) {
|
||||
for (Band band : Band.values()) {
|
||||
if (band == expected) {
|
||||
assertTrue(member.isWorkedOnBand(band), band.name());
|
||||
} else {
|
||||
assertFalse(member.isWorkedOnBand(band), band.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ChatMember memberWith(java.util.function.Consumer<ChatMember> setup) {
|
||||
ChatMember member = new ChatMember();
|
||||
setup.accept(member);
|
||||
return member;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user