mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-08-23 18:47:34 +02:00
frequency-recognition: explicit frequencdies out of the name field will be copied to the qrg field and trigger the used band spot of a station to fit for this qrg. Airscout will then use this band explicitely for the calculation
This commit is contained in:
@@ -34,7 +34,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import kst4contest.logic.FrequencyTextParser;
|
||||
|
||||
|
||||
|
||||
@@ -1490,6 +1490,38 @@ private ObservableList<String>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Uses one unambiguous explicit QRG from the current station name as the
|
||||
* initial compatibility frequency.
|
||||
*
|
||||
* <p>Multiple explicit QRGs are valid band evidence, but there is no safe
|
||||
* assumption which one is the current run frequency. Therefore the legacy
|
||||
* frequency property is initialized only when exactly one explicit QRG exists.</p>
|
||||
*/
|
||||
private void initializeFrequencyFromStationNameIfUnambiguous(
|
||||
ChatMember member
|
||||
) {
|
||||
if (member == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<FrequencyTextParser.DetectedFrequency> detectedFrequencies =
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
member.getName()
|
||||
);
|
||||
|
||||
if (detectedFrequencies.size() != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
member.initializeFrequencyIfEmpty(
|
||||
detectedFrequencies
|
||||
.get(0)
|
||||
.getFrequencyMHz()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or replaces an active chat member in the worker-thread model and mirrors
|
||||
* that change to the JavaFX list. This is the only supported path for ON4KST
|
||||
@@ -1501,6 +1533,8 @@ private ObservableList<String>
|
||||
return;
|
||||
}
|
||||
|
||||
initializeFrequencyFromStationNameIfUnambiguous(member);
|
||||
|
||||
activeChatMembersByCallAndCategory.put(key, member);
|
||||
|
||||
runOnFxThread(() -> {
|
||||
@@ -1683,6 +1717,9 @@ private ObservableList<String>
|
||||
}
|
||||
|
||||
activeMember.setName(updatedMember.getName());
|
||||
initializeFrequencyFromStationNameIfUnambiguous(
|
||||
activeMember
|
||||
);
|
||||
activeMember.setQra(updatedMember.getQra());
|
||||
activeMember.setState(updatedMember.getState());
|
||||
activeMember.setLastActivity(updatedMember.getLastActivity());
|
||||
|
||||
@@ -18,6 +18,7 @@ import kst4contest.ApplicationConstants;
|
||||
import kst4contest.locatorUtils.DirectionUtils;
|
||||
import kst4contest.locatorUtils.Location;
|
||||
import kst4contest.model.*;
|
||||
import kst4contest.logic.FrequencyTextParser;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -58,7 +59,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
* would be converted into plausible but incorrect frequencies.
|
||||
*/
|
||||
private static final Pattern SMART_FREQUENCY_PATTERN = Pattern.compile(
|
||||
"(?<![\\d])(\\d{3,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)(?![\\d])"
|
||||
"(?<![\\d])(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)(?![\\d])"
|
||||
+ "|(?<![\\d])([.,]\\d{3}(?:[.,]\\d{1,3})?)(?![\\d])"
|
||||
+ "|(?<=\\s|^)(\\d{3})(?=\\s|$)"
|
||||
);
|
||||
@@ -319,16 +320,28 @@ public class MessageBusManagementThread extends Thread {
|
||||
|
||||
try {
|
||||
String reconstructed =
|
||||
candidateBand.getPrefix() + "." + foundRaw;
|
||||
double candidateFrequency = Double.parseDouble(
|
||||
normalizeFrequencyString(reconstructed)
|
||||
);
|
||||
candidateBand.getPrefix()
|
||||
+ "."
|
||||
+ foundRaw;
|
||||
|
||||
if (candidateBand.isPlausible(candidateFrequency)
|
||||
FrequencyTextParser.DetectedFrequency detectedFrequency =
|
||||
FrequencyTextParser.parseExplicitFrequency(
|
||||
reconstructed
|
||||
);
|
||||
|
||||
if (detectedFrequency != null
|
||||
&& detectedFrequency.getBand() == candidateBand
|
||||
&& info.timestampEpoch > bestTimestamp) {
|
||||
finalDetectedFrequency = candidateFrequency;
|
||||
finalDetectedBand = candidateBand;
|
||||
bestTimestamp = info.timestampEpoch;
|
||||
|
||||
finalDetectedFrequency =
|
||||
detectedFrequency.getFrequencyMHz();
|
||||
|
||||
finalDetectedBand =
|
||||
candidateBand;
|
||||
|
||||
bestTimestamp =
|
||||
info.timestampEpoch;
|
||||
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// Try the next known band.
|
||||
@@ -357,27 +370,40 @@ public class MessageBusManagementThread extends Thread {
|
||||
|
||||
try {
|
||||
String reconstructed =
|
||||
fallbackBand.getPrefix() + "." + foundRaw;
|
||||
double candidateFrequency = Double.parseDouble(
|
||||
normalizeFrequencyString(reconstructed)
|
||||
);
|
||||
fallbackBand.getPrefix()
|
||||
+ "."
|
||||
+ foundRaw;
|
||||
|
||||
if (fallbackBand.isPlausible(candidateFrequency)) {
|
||||
finalDetectedFrequency = candidateFrequency;
|
||||
finalDetectedBand = fallbackBand;
|
||||
FrequencyTextParser.DetectedFrequency detectedFrequency =
|
||||
FrequencyTextParser.parseExplicitFrequency(
|
||||
reconstructed
|
||||
);
|
||||
|
||||
if (detectedFrequency != null
|
||||
&& detectedFrequency.getBand() == fallbackBand) {
|
||||
|
||||
finalDetectedFrequency =
|
||||
detectedFrequency.getFrequencyMHz();
|
||||
|
||||
finalDetectedBand =
|
||||
fallbackBand;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// The matched value cannot be converted into a frequency.
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
finalDetectedFrequency = Double.parseDouble(
|
||||
normalizeFrequencyString(foundRaw)
|
||||
);
|
||||
finalDetectedBand = Band.fromFrequency(finalDetectedFrequency);
|
||||
} catch (NumberFormatException ignored) {
|
||||
// Continue with the next possible match in the message.
|
||||
FrequencyTextParser.DetectedFrequency detectedFrequency =
|
||||
FrequencyTextParser.parseExplicitFrequency(
|
||||
foundRaw
|
||||
);
|
||||
|
||||
if (detectedFrequency != null) {
|
||||
finalDetectedFrequency =
|
||||
detectedFrequency.getFrequencyMHz();
|
||||
|
||||
finalDetectedBand =
|
||||
detectedFrequency.getBand();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,22 +488,22 @@ public class MessageBusManagementThread extends Thread {
|
||||
* Example: "144.210.10" -> "144.21010"
|
||||
* Example: "144.210" -> "144.210"
|
||||
*/
|
||||
private String normalizeFrequencyString(String rawInput) {
|
||||
// Input is already guaranteed to have only dots as separators (commas replaced earlier)
|
||||
|
||||
int firstDotIndex = rawInput.indexOf(".");
|
||||
|
||||
if (firstDotIndex != -1) {
|
||||
// Check if there are more dots after the first one
|
||||
String decimalPart = rawInput.substring(firstDotIndex + 1);
|
||||
if (decimalPart.contains(".")) {
|
||||
// Remove all subsequent dots to make it a valid double
|
||||
decimalPart = decimalPart.replace(".", "");
|
||||
return rawInput.substring(0, firstDotIndex) + "." + decimalPart;
|
||||
}
|
||||
}
|
||||
return rawInput;
|
||||
}
|
||||
// private String normalizeFrequencyString(String rawInput) {
|
||||
// // Input is already guaranteed to have only dots as separators (commas replaced earlier)
|
||||
//
|
||||
// int firstDotIndex = rawInput.indexOf(".");
|
||||
//
|
||||
// if (firstDotIndex != -1) {
|
||||
// // Check if there are more dots after the first one
|
||||
// String decimalPart = rawInput.substring(firstDotIndex + 1);
|
||||
// if (decimalPart.contains(".")) {
|
||||
// // Remove all subsequent dots to make it a valid double
|
||||
// decimalPart = decimalPart.replace(".", "");
|
||||
// return rawInput.substring(0, firstDotIndex) + "." + decimalPart;
|
||||
// }
|
||||
// }
|
||||
// return rawInput;
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -113,12 +113,32 @@ public final class BandOpportunityResolver {
|
||||
return detectedBands;
|
||||
}
|
||||
|
||||
/*
|
||||
* Explicit band descriptions:
|
||||
* 2m, 70cm, 23cm, 432, 1296, ...
|
||||
*/
|
||||
for (Map.Entry<Band, Pattern> entry : STATION_NAME_BAND_PATTERNS.entrySet()) {
|
||||
if (entry.getValue().matcher(stationName).find()) {
|
||||
detectedBands.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Explicit full QRGs in name field:
|
||||
* 432.357 -> 432 MHz
|
||||
* 1296.210 -> 1296 MHz
|
||||
* etc.
|
||||
*/
|
||||
for (FrequencyTextParser.DetectedFrequency detectedFrequency
|
||||
: FrequencyTextParser.findExplicitFrequencies(
|
||||
stationName
|
||||
)) {
|
||||
|
||||
detectedBands.add(
|
||||
detectedFrequency.getBand()
|
||||
);
|
||||
}
|
||||
|
||||
return detectedBands;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package kst4contest.logic;
|
||||
|
||||
import kst4contest.model.Band;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Common parser for explicit amateur-radio frequencies embedded in text.
|
||||
*
|
||||
* <p>This parser deliberately handles only complete frequencies such as
|
||||
* 144.300, 432.357 or 10368.100. Relative forms such as ".210" or ambiguous
|
||||
* bare values such as "210" require additional message context and remain the
|
||||
* responsibility of the chat-message parser.</p>
|
||||
*/
|
||||
public final class FrequencyTextParser {
|
||||
|
||||
/*
|
||||
* Examples:
|
||||
* 50.150
|
||||
* 144.300
|
||||
* 432,357
|
||||
* 10368.100
|
||||
* 144.300.03
|
||||
*
|
||||
* At least two digits are required before the decimal separator. This
|
||||
* intentionally prevents "1.2" from being interpreted as a frequency.
|
||||
*/
|
||||
private static final Pattern EXPLICIT_FREQUENCY_PATTERN = Pattern.compile(
|
||||
"(?<![A-Z0-9])"
|
||||
+ "(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)"
|
||||
+ "(?!\\d)",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
private FrequencyTextParser() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all distinct explicit frequencies that fall into one of the bands
|
||||
* supported by {@link Band}.
|
||||
*/
|
||||
public static List<DetectedFrequency> findExplicitFrequencies(String text) {
|
||||
if (text == null || text.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
Matcher matcher = EXPLICIT_FREQUENCY_PATTERN.matcher(text);
|
||||
|
||||
Map<String, DetectedFrequency> uniqueFrequencies =
|
||||
new LinkedHashMap<>();
|
||||
|
||||
while (matcher.find()) {
|
||||
DetectedFrequency detected =
|
||||
parseExplicitFrequency(matcher.group(1));
|
||||
|
||||
if (detected == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String uniqueKey =
|
||||
detected.getBand().name()
|
||||
+ "|"
|
||||
+ Double.toString(
|
||||
detected.getFrequencyMHz()
|
||||
);
|
||||
|
||||
uniqueFrequencies.putIfAbsent(
|
||||
uniqueKey,
|
||||
detected
|
||||
);
|
||||
}
|
||||
|
||||
return List.copyOf(
|
||||
new ArrayList<>(uniqueFrequencies.values())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses one complete frequency.
|
||||
*
|
||||
* @return detected band/frequency or {@code null} when the value is invalid
|
||||
* or outside the supported amateur bands
|
||||
*/
|
||||
public static DetectedFrequency parseExplicitFrequency(
|
||||
String rawFrequency
|
||||
) {
|
||||
if (rawFrequency == null || rawFrequency.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalized =
|
||||
normalizeFrequencyString(
|
||||
rawFrequency
|
||||
.trim()
|
||||
.replace(',', '.')
|
||||
);
|
||||
|
||||
try {
|
||||
double frequencyMHz =
|
||||
Double.parseDouble(normalized);
|
||||
|
||||
if (!Double.isFinite(frequencyMHz)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Band band =
|
||||
Band.fromFrequency(frequencyMHz);
|
||||
|
||||
if (band == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DetectedFrequency(
|
||||
band,
|
||||
frequencyMHz,
|
||||
rawFrequency.trim()
|
||||
);
|
||||
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes KST-style frequency strings with an optional second decimal
|
||||
* separator.
|
||||
*
|
||||
* Examples:
|
||||
* 144.300.03 -> 144.30003
|
||||
* 144.300 -> 144.300
|
||||
*/
|
||||
private static String normalizeFrequencyString(
|
||||
String rawInput
|
||||
) {
|
||||
int firstDotIndex = rawInput.indexOf('.');
|
||||
|
||||
if (firstDotIndex < 0) {
|
||||
return rawInput;
|
||||
}
|
||||
|
||||
String decimalPart =
|
||||
rawInput.substring(firstDotIndex + 1);
|
||||
|
||||
if (!decimalPart.contains(".")) {
|
||||
return rawInput;
|
||||
}
|
||||
|
||||
decimalPart =
|
||||
decimalPart.replace(".", "");
|
||||
|
||||
return rawInput.substring(0, firstDotIndex + 1)
|
||||
+ decimalPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable result of one explicit-frequency detection.
|
||||
*/
|
||||
public static final class DetectedFrequency {
|
||||
|
||||
private final Band band;
|
||||
private final double frequencyMHz;
|
||||
private final String sourceText;
|
||||
|
||||
private DetectedFrequency(
|
||||
Band band,
|
||||
double frequencyMHz,
|
||||
String sourceText
|
||||
) {
|
||||
this.band = band;
|
||||
this.frequencyMHz = frequencyMHz;
|
||||
this.sourceText = sourceText;
|
||||
}
|
||||
|
||||
public Band getBand() {
|
||||
return band;
|
||||
}
|
||||
|
||||
public double getFrequencyMHz() {
|
||||
return frequencyMHz;
|
||||
}
|
||||
|
||||
public String getSourceText() {
|
||||
return sourceText;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ public final class PropagationFrequencyResolver {
|
||||
/** Explains why a frequency was selected. */
|
||||
public enum Source {
|
||||
CURRENT_QRG,
|
||||
STATION_NAME_QRG,
|
||||
STATION_NAME,
|
||||
DUAL_CATEGORY_FALLBACK,
|
||||
CHAT_CATEGORY
|
||||
@@ -90,6 +91,20 @@ public final class PropagationFrequencyResolver {
|
||||
);
|
||||
}
|
||||
|
||||
FrequencyCandidate stationNameQrg =
|
||||
findUniqueStationNameQrg(
|
||||
supportedVariants,
|
||||
usableBands
|
||||
);
|
||||
|
||||
if (stationNameQrg != null) {
|
||||
return new Resolution(
|
||||
stationNameQrg.band,
|
||||
stationNameQrg.frequencyMHz,
|
||||
Source.STATION_NAME_QRG
|
||||
);
|
||||
}
|
||||
|
||||
EnumSet<Band> nameBands = EnumSet.noneOf(Band.class);
|
||||
for (ChatMember variant : supportedVariants) {
|
||||
nameBands.addAll(
|
||||
@@ -172,6 +187,73 @@ public final class PropagationFrequencyResolver {
|
||||
return latest;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolves one exact station-name QRG only when the evidence is unambiguous.
|
||||
*
|
||||
* <p>The same QRG repeated in several category variants counts only once.
|
||||
* If different explicit QRGs are advertised, no run frequency is guessed and
|
||||
* the caller continues with normal band-name/category resolution.</p>
|
||||
*/
|
||||
private static FrequencyCandidate findUniqueStationNameQrg(
|
||||
List<ChatMember> variants,
|
||||
EnumSet<Band> usableBands
|
||||
) {
|
||||
java.util.LinkedHashMap<String, FrequencyCandidate> uniqueCandidates =
|
||||
new java.util.LinkedHashMap<>();
|
||||
|
||||
for (ChatMember variant : variants) {
|
||||
if (variant == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (FrequencyTextParser.DetectedFrequency detectedFrequency
|
||||
: FrequencyTextParser.findExplicitFrequencies(
|
||||
variant.getName()
|
||||
)) {
|
||||
|
||||
Band band =
|
||||
detectedFrequency.getBand();
|
||||
|
||||
if (!usableBands.contains(band)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double frequencyMHz =
|
||||
detectedFrequency.getFrequencyMHz();
|
||||
|
||||
String key =
|
||||
band.name()
|
||||
+ "|"
|
||||
+ Double.toString(frequencyMHz);
|
||||
|
||||
uniqueCandidates.putIfAbsent(
|
||||
key,
|
||||
new FrequencyCandidate(
|
||||
band,
|
||||
frequencyMHz,
|
||||
Long.MIN_VALUE
|
||||
)
|
||||
);
|
||||
|
||||
/*
|
||||
* More than one different exact QRG means that we cannot safely
|
||||
* identify one run frequency.
|
||||
*/
|
||||
if (uniqueCandidates.size() > 1) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueCandidates.size() == 1
|
||||
? uniqueCandidates.values()
|
||||
.iterator()
|
||||
.next()
|
||||
: null;
|
||||
}
|
||||
|
||||
|
||||
private static boolean isSupportedVariant(ChatMember member) {
|
||||
if (member == null || member.getChatCategory() == null) {
|
||||
return false;
|
||||
|
||||
@@ -518,6 +518,7 @@ public class ChatMember {
|
||||
this.frequency = frequency;
|
||||
}
|
||||
|
||||
|
||||
public long getActivityTimeLastInEpoch() {
|
||||
return activityTimeLastInEpoch;
|
||||
}
|
||||
@@ -595,6 +596,40 @@ public class ChatMember {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the compatibility frequency only when no frequency is known yet.
|
||||
*
|
||||
* <p>This is used for an explicit QRG contained in the ON4KST station name.
|
||||
* A subsequently detected chat QRG may overwrite this initial value, but a
|
||||
* station-name QRG never replaces an already known frequency.</p>
|
||||
*
|
||||
* @param frequencyMhz explicit station-name frequency
|
||||
* @return true when the frequency was initialized
|
||||
*/
|
||||
public boolean initializeFrequencyIfEmpty(double frequencyMhz) {
|
||||
if (!Double.isFinite(frequencyMhz)
|
||||
|| frequencyMhz <= 0.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (frequency == null) {
|
||||
frequency = new SimpleStringProperty();
|
||||
}
|
||||
|
||||
String currentFrequency = frequency.get();
|
||||
|
||||
if (currentFrequency != null
|
||||
&& !currentFrequency.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
frequency.set(
|
||||
Double.toString(frequencyMhz)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all worked information of this object to false. Scope: GUI, Reset Button
|
||||
* for worked info, called by appcontroller
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.logic.FrequencyTextParser;
|
||||
import kst4contest.model.Band;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class FrequencyTextParserTest {
|
||||
|
||||
@Test
|
||||
void detectsExplicitStationNameFrequencies() {
|
||||
List<FrequencyTextParser.DetectedFrequency> detected =
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
"Phil 432.357"
|
||||
);
|
||||
|
||||
assertEquals(1, detected.size());
|
||||
assertEquals(
|
||||
Band.B_432,
|
||||
detected.get(0).getBand()
|
||||
);
|
||||
assertEquals(
|
||||
432.357,
|
||||
detected.get(0).getFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsCommaAndMicrowaveFrequencies() {
|
||||
List<FrequencyTextParser.DetectedFrequency> detected =
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
"QRV 1296,210 / 10368.100"
|
||||
);
|
||||
|
||||
assertEquals(2, detected.size());
|
||||
|
||||
assertEquals(
|
||||
Band.B_1296,
|
||||
detected.get(0).getBand()
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Band.B_10G,
|
||||
detected.get(1).getBand()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsFiftyAndSeventyMhzFrequencies() {
|
||||
assertEquals(
|
||||
Band.B_50,
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies("50.150")
|
||||
.get(0)
|
||||
.getBand()
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Band.B_70,
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies("70.200")
|
||||
.get(0)
|
||||
.getBand()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresRelativeAndAmbiguousValues() {
|
||||
assertTrue(
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"Mike .180"
|
||||
)
|
||||
.isEmpty()
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"Mike 180"
|
||||
)
|
||||
.isEmpty()
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"David 1.2"
|
||||
)
|
||||
.isEmpty()
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"RST 599"
|
||||
)
|
||||
.isEmpty()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizesSubKhzNotation() {
|
||||
FrequencyTextParser.DetectedFrequency detected =
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"144.300.03"
|
||||
)
|
||||
.get(0);
|
||||
|
||||
assertEquals(Band.B_144, detected.getBand());
|
||||
assertEquals(
|
||||
144.30003,
|
||||
detected.getFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import kst4contest.logic.FrequencyTextParser;
|
||||
|
||||
/**
|
||||
* Builds immutable map snapshots from the currently visible chat members.
|
||||
@@ -204,6 +205,92 @@ public final class MapCallsignRawSnapshotBuilder {
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* A QRG explicitly contained in the current station name does not expire.
|
||||
*
|
||||
* Recent dynamic QRG evidence has priority. Therefore station-name QRGs fill
|
||||
* only bands for which no recent dynamic frequency is available.
|
||||
*
|
||||
* Different explicit QRGs for the same band are considered ambiguous and are
|
||||
* not reduced to one arbitrary run frequency.
|
||||
*/
|
||||
Map<Band, Map<String, FrequencyTextParser.DetectedFrequency>>
|
||||
stationNameFrequenciesByBand =
|
||||
new EnumMap<>(Band.class);
|
||||
|
||||
for (ChatMember variant : variants) {
|
||||
if (variant == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (FrequencyTextParser.DetectedFrequency detectedFrequency
|
||||
: FrequencyTextParser.findExplicitFrequencies(
|
||||
variant.getName()
|
||||
)) {
|
||||
|
||||
Band band =
|
||||
detectedFrequency.getBand();
|
||||
|
||||
if (availableBands == null
|
||||
|| !availableBands.contains(band)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
stationNameFrequenciesByBand
|
||||
.computeIfAbsent(
|
||||
band,
|
||||
ignored -> new LinkedHashMap<>()
|
||||
)
|
||||
.putIfAbsent(
|
||||
Double.toString(
|
||||
detectedFrequency.getFrequencyMHz()
|
||||
),
|
||||
detectedFrequency
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<
|
||||
Band,
|
||||
Map<String, FrequencyTextParser.DetectedFrequency>>
|
||||
entry : stationNameFrequenciesByBand.entrySet()) {
|
||||
|
||||
Band band = entry.getKey();
|
||||
|
||||
/*
|
||||
* A recent QRG detected from chat always wins.
|
||||
*/
|
||||
if (latestByBand.containsKey(band)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Do not guess when several different QRGs were published for the
|
||||
* same band.
|
||||
*/
|
||||
if (entry.getValue().size() != 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FrequencyTextParser.DetectedFrequency detectedFrequency =
|
||||
entry.getValue()
|
||||
.values()
|
||||
.iterator()
|
||||
.next();
|
||||
|
||||
latestByBand.put(
|
||||
band,
|
||||
new FrequencyCandidate(
|
||||
band,
|
||||
formatFrequency(
|
||||
detectedFrequency.getFrequencyMHz()
|
||||
),
|
||||
Long.MIN_VALUE
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
LinkedHashMap<String, String> ordered = new LinkedHashMap<>();
|
||||
|
||||
@@ -14,6 +14,39 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class BandOpportunityResolverTest {
|
||||
|
||||
@Test
|
||||
void explicitStationNameFrequencyProvidesBandEvidence() {
|
||||
ChatMember station = new ChatMember();
|
||||
station.setName("Phil 432.357");
|
||||
|
||||
BandOpportunityResolver.Resolution resolution =
|
||||
BandOpportunityResolver.resolve(
|
||||
List.of(station),
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
EnumSet.of(Band.B_432),
|
||||
resolution.getOfferedBands()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void relativeStationNameFrequencyDoesNotProvideBandEvidence() {
|
||||
ChatMember station = new ChatMember();
|
||||
station.setName("Mike .180");
|
||||
|
||||
BandOpportunityResolver.Resolution resolution =
|
||||
BandOpportunityResolver.resolve(
|
||||
List.of(station),
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
resolution.getOfferedBands().isEmpty()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesCommonShorthandBandsFromStationName() {
|
||||
EnumSet<Band> bands = BandOpportunityResolver.detectBandsFromStationName(
|
||||
|
||||
@@ -15,6 +15,70 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class MapCallsignRawSnapshotBuilderTest {
|
||||
|
||||
@Test
|
||||
void explicitStationNameQrgIsShownInMapSnapshot() {
|
||||
ChatMember station =
|
||||
buildStation(
|
||||
"G0JSB",
|
||||
"Phil 432.357",
|
||||
"IO91AA",
|
||||
1_000L
|
||||
);
|
||||
|
||||
MapCallsignRawSnapshot snapshot =
|
||||
new MapCallsignRawSnapshotBuilder()
|
||||
.buildSnapshots(
|
||||
List.of(station),
|
||||
null,
|
||||
EnumSet.of(Band.B_432)
|
||||
)
|
||||
.get(0);
|
||||
|
||||
assertEquals(
|
||||
"432",
|
||||
snapshot.bandSummary()
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"432.357",
|
||||
snapshot
|
||||
.lastKnownFrequenciesByBand()
|
||||
.get("432")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void recentDynamicQrgWinsOverStationNameQrgInMap() {
|
||||
ChatMember station =
|
||||
buildStation(
|
||||
"G0JSB",
|
||||
"Phil 432.357",
|
||||
"IO91AA",
|
||||
1_000L
|
||||
);
|
||||
|
||||
station.addKnownFrequency(
|
||||
Band.B_432,
|
||||
432.335
|
||||
);
|
||||
|
||||
MapCallsignRawSnapshot snapshot =
|
||||
new MapCallsignRawSnapshotBuilder()
|
||||
.buildSnapshots(
|
||||
List.of(station),
|
||||
null,
|
||||
EnumSet.of(Band.B_432)
|
||||
)
|
||||
.get(0);
|
||||
|
||||
assertEquals(
|
||||
"432.335",
|
||||
snapshot
|
||||
.lastKnownFrequenciesByBand()
|
||||
.get("432")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void marksSnapshotWhenNameAdvertisesSelectedBand() {
|
||||
ChatMember station = buildStation("DL1ABC", "QRV 2-70-23", "JN58TD", 1_000L);
|
||||
|
||||
@@ -16,6 +16,113 @@ class PropagationFrequencyResolverTest {
|
||||
|
||||
private static final long NOW = 10_000_000L;
|
||||
|
||||
|
||||
@Test
|
||||
void exactStationNameQrgWinsOverBandAndCategoryFallback() {
|
||||
ChatMember station =
|
||||
station(
|
||||
ChatCategory.VUHF,
|
||||
"Phil 432.357"
|
||||
);
|
||||
|
||||
PropagationFrequencyResolver.Resolution resolution =
|
||||
resolve(
|
||||
List.of(station),
|
||||
EnumSet.of(
|
||||
Band.B_144,
|
||||
Band.B_432
|
||||
)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Band.B_432,
|
||||
resolution.getBand()
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
432.357,
|
||||
resolution.getAnalysisFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
PropagationFrequencyResolver.Source.STATION_NAME_QRG,
|
||||
resolution.getSource()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void recentChatQrgWinsOverExactStationNameQrg() {
|
||||
ChatMember station =
|
||||
station(
|
||||
ChatCategory.VUHF,
|
||||
"Phil 432.357"
|
||||
);
|
||||
|
||||
addCurrentQrg(
|
||||
station,
|
||||
Band.B_432,
|
||||
432.335,
|
||||
NOW - 1_000L
|
||||
);
|
||||
|
||||
PropagationFrequencyResolver.Resolution resolution =
|
||||
resolve(
|
||||
List.of(station),
|
||||
EnumSet.of(Band.B_432)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
432.335,
|
||||
resolution.getAnalysisFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
PropagationFrequencyResolver.Source.CURRENT_QRG,
|
||||
resolution.getSource()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleStationNameQrgsAreNotTreatedAsOneRunFrequency() {
|
||||
ChatMember station =
|
||||
station(
|
||||
ChatCategory.EMEJT65,
|
||||
"QRV 432.357 1296.210"
|
||||
);
|
||||
|
||||
PropagationFrequencyResolver.Resolution resolution =
|
||||
resolve(
|
||||
List.of(station),
|
||||
EnumSet.of(
|
||||
Band.B_432,
|
||||
Band.B_1296
|
||||
)
|
||||
);
|
||||
|
||||
/*
|
||||
* Both bands are known, but no exact run QRG is guessed.
|
||||
* Existing station-name band selection therefore chooses the
|
||||
* lowest usable advertised band.
|
||||
*/
|
||||
assertEquals(
|
||||
Band.B_432,
|
||||
resolution.getBand()
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
432.0,
|
||||
resolution.getAnalysisFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
PropagationFrequencyResolver.Source.STATION_NAME,
|
||||
resolution.getSource()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void currentQrgWinsOverNameAndCategoryFallback() {
|
||||
ChatMember station = station(ChatCategory.MICROWAVE, "QRV 3cm");
|
||||
|
||||
Reference in New Issue
Block a user