mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-08-24 11:06:59 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2828e7ef80 | ||
|
|
b0cc5ee9ba |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@@ -177,16 +177,106 @@ Herleitung und Grenzen: [Prioritätsscore und Prioritätsliste](de-Funktionen#pr
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Cluster & QSO der anderen
|
## Stationskarte
|
||||||
|
|
||||||
Separates Fenster (kann miniaturisiert werden). Zeigt den Kommunikationsfluss zwischen anderen Stationen – interessant in ruhigeren Phasen.
|
Die Stationskarte kann auf zwei Wegen geöffnet werden:
|
||||||
|
|
||||||
|
- **Windows → Show / hide station map** öffnet oder schließt das Kartenfenster.
|
||||||
|
- **Show on map** im **Further Info**-Bereich öffnet die Karte und zentriert sie auf die ausgewählte Station.
|
||||||
|
|
||||||
|
Die Karte zeigt die Stationen, die auch nach Anwendung der aktuellen Benutzerlistenfilter noch sichtbar sind. Ein Hinweis in der Kopfzeile zeigt an, wenn eine gefilterte Ansicht aktiv ist.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Ein einzelner Stationsmarker kann direkt angeklickt werden. KST4Contest übernimmt die Station daraufhin als aktuelle Auswahl, scrollt die Benutzerliste zum passenden Chatmember und aktualisiert den **Further Info**-Bereich.
|
||||||
|
|
||||||
|
Marker, die bei der aktuellen Zoomstufe zu dicht beieinanderliegen, werden als Cluster mit einer Stationsanzahl angezeigt. Ein Klick auf einen Cluster vergrößert den betreffenden Kartenausschnitt. Erst ein anschließend sichtbarer einzelner Marker wählt eine konkrete Station aus.
|
||||||
|
|
||||||
|
Für die ausgewählte Station erscheinen rechts unter **Selected station**:
|
||||||
|
|
||||||
|
- Rufzeichen,
|
||||||
|
- Locator,
|
||||||
|
- QRB und QTF,
|
||||||
|
- erkannte aktive Bänder,
|
||||||
|
- gegebenenfalls `B+` für eine offene Bandmöglichkeit und
|
||||||
|
- die zuletzt bekannten QRGs.
|
||||||
|
|
||||||
|
**Trigger cluster spot** sendet für die ausgewählte Station einen einzelnen Spot an die mit dem integrierten DX-Cluster-Server verbundenen Logprogramme. Die Schaltfläche setzt deshalb einen aktivierten Cluster-Server und mindestens einen verbundenen Client voraus.
|
||||||
|
|
||||||
|
Unterhalb der Karte befindet sich das Höhenprofil. Rechts werden die dazugehörigen Detailwerte angezeigt, unter anderem:
|
||||||
|
|
||||||
|
- verwendete Datenquelle und Anzahl der Höhenpunkte,
|
||||||
|
- Analysefrequenz,
|
||||||
|
- Erdkrümmungs- beziehungsweise Refraktionsmodell,
|
||||||
|
- Radio- und Geländehorizont,
|
||||||
|
- Fresnel-Freiheit,
|
||||||
|
- erkannte Hindernisse,
|
||||||
|
- Link-Budget,
|
||||||
|
- geschätzter Empfangspegel und
|
||||||
|
- eine zusammenfassende Pfadbewertung.
|
||||||
|
|
||||||
|
Mit **Hide path analysis** werden das Profil unterhalb der Karte und die ausführlichen Analysewerte rechts gemeinsam ausgeblendet. Der Kartenbereich erhält dadurch mehr Platz.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Der Hinweis **Path analysis is hidden** bleibt zusammen mit **Show path analysis** sichtbar. Die Funktion kann daher ohne Umweg wieder eingeschaltet werden. Der Zustand wird gespeichert.
|
||||||
|
|
||||||
|
Der Divider zwischen Karte und Detailbereich lässt sich horizontal verschieben. Bei schmalem Detailbereich werden längere Angaben umgebrochen; falls die Höhe nicht ausreicht, erscheint dort eine vertikale Scrollleiste.
|
||||||
|
|
||||||
|
Ausführliche Herleitung und Grenzen: [Stationskarte und Streckenanalyse](de-Funktionen#stationskarte-und-streckenanalyse-ab-v141)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Globale Nachrichtentabs und Monitorfenster
|
||||||
|
|
||||||
|
Der untere Bereich des Hauptfensters enthält drei globale Nachrichtentabs. Ihr Inhalt ist nicht von der aktuell in der Benutzerliste ausgewählten Station abhängig.
|
||||||
|
|
||||||
|
| Tab | Inhalt |
|
||||||
|
|---|---|
|
||||||
|
| **Public messages** | Öffentliche Chatnachrichten, CQ-Rufe und Beacons |
|
||||||
|
| **DXCluster messages** | Über ON4KST empfangene DX-Cluster-Meldungen |
|
||||||
|
| **QSO of the other** | Gerichtete Nachrichten zwischen zwei anderen Stationen |
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Im Tab **QSO of the other** werden Absender und Empfänger getrennt dargestellt. Die Spalten **Last QRG TX** und **Last QRG RX** enthalten die zuletzt für beide Stationen bekannten Frequenzen. Sie geben nicht zwingend die QRG der angezeigten Unterhaltung wieder.
|
||||||
|
|
||||||
|
**wkd TX?** und **wkd RX?** zeigen den globalen Worked-Status der beiden Basisrufzeichen. Die Angaben sind nicht bandbezogen.
|
||||||
|
|
||||||
|
Der Tab **DXCluster messages** zeigt den meldenden und den gemeldeten Teilnehmer, deren Locator, die QRG, den Meldungstext und den globalen Worked-Status der gemeldeten Station. Welche Felder tatsächlich gefüllt sind, hängt von der vom ON4KST-Server übertragenen Meldung ab.
|
||||||
|
|
||||||
|
Nachrichtentexte bleiben einzeilig. Ist eine Zelle zu schmal, erscheint der vollständige Inhalt als Tooltip. Webadressen im Meldungstext lassen sich anklicken.
|
||||||
|
|
||||||
|
### Separates Monitorfenster
|
||||||
|
|
||||||
|
Zusätzlich öffnet KST4Contest das Fenster **Cluster & QSO of the other**. Es zeigt oben die DX-Cluster-Meldungen und darunter die gerichteten Nachrichten zwischen anderen Stationen.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Die Position des vertikalen Dividers sowie die Fenstergröße werden zusammen mit den übrigen UI-Einstellungen gespeichert. Nach einer Änderung **Save Settings** verwenden.
|
||||||
|
|
||||||
|
Das Fenster lässt sich über das Menü aus- und wieder einblenden:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Windows → Hide cluster / stranger QSOs
|
||||||
|
Windows → Show cluster / stranger QSOs
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Tabellen im Hauptfenster und im Monitorfenster greifen auf dieselben Daten zu. Das Ausblenden des Monitorfensters beendet daher weder den Empfang noch die Darstellung in den unteren Tabs.
|
||||||
|
|
||||||
|
Herleitung und Grenzen: [Globale Nachrichtenansichten](de-Funktionen#globale-nachrichtenansichten).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Menü
|
## Menü
|
||||||
|
|
||||||
### Window
|
### Windows
|
||||||
- **Use Dark Mode** (ab v1.26): Dunkles Farbschema aktivieren/deaktivieren.
|
|
||||||
|
- **Hide cluster / stranger QSOs** beziehungsweise **Show cluster / stranger QSOs**: Blendet das zusätzliche Cluster- und QSO-Monitorfenster aus oder wieder ein.
|
||||||
|
- **hide options** beziehungsweise **show options**: Blendet das Einstellungsfenster aus oder wieder ein.
|
||||||
|
- **Use dark mode design**: Aktiviert das dunkle Farbschema.
|
||||||
|
- **Use default mode design**: Aktiviert das normale helle Farbschema.
|
||||||
|
- **Show / hide station map** öffnet beziehungsweise schließt das separate Fenster mit Stationskarte und Streckenanalyse.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+211
-17
@@ -614,39 +614,233 @@ Der Beacon ist für längeres CQ-Rufen auf einer festen Frequenz gedacht. Beim A
|
|||||||
|
|
||||||
## Simplelogfile
|
## Simplelogfile
|
||||||
|
|
||||||
Dateibasierte Log-Auswertung per Regex. Details: [Log-Synchronisation](Log-Synchronisation#methode-1-universal-file-based-callsign-interpreter-simplelogfile).
|
Details: [Log-Synchronisation](de-Log-Synchronisation#methode-1-universal-file-based-callsign-interpreter-simplelogfile).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Cluster & QSO der anderen
|
## Globale Nachrichtenansichten
|
||||||
|
|
||||||
Ein separates Fenster zeigt den QSO-Fluss zwischen anderen Stationen. Besonders interessant in ruhigeren Nacht-Stunden während des Contests, wenn weniger Verkehr herrscht.
|
Das Stationsinfo-Panel und die PM-Tabelle beantworten Fragen zu einer bestimmten Station oder zur eigenen Kommunikation. Daneben gibt es Nachrichtenströme, die unabhängig von der aktuell ausgewählten Station betrachtet werden müssen.
|
||||||
|
|
||||||
Dieses Fenster kann minimiert werden, wenn es nicht benötigt wird. Zukünftig geplant: Filterung auf Stationen im ausgewählten QTF.
|
KST4Contest fasst diese globalen Informationen in drei Ansichten zusammen:
|
||||||
|
|
||||||
|
| Ansicht | Inhalt |
|
||||||
|
|---|---|
|
||||||
|
| **Public messages** | Öffentliche Chatnachrichten, CQ-Rufe und Beacons |
|
||||||
|
| **DXCluster messages** | Vom ON4KST-Server gelieferte DX-Cluster-Meldungen |
|
||||||
|
| **QSO of the other** | Gerichtete Chatnachrichten zwischen zwei anderen Stationen |
|
||||||
|
|
||||||
|
Die Ansichten befinden sich als Tabs im unteren Bereich des Hauptfensters. **Public messages** ist nach dem Programmstart vorausgewählt.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### DXCluster messages
|
||||||
|
|
||||||
|
Der Tab **DXCluster messages** zeigt DX-Cluster-Meldungen, die über die bestehende ON4KST-Verbindung empfangen werden. Je nach Inhalt der Meldung stehen folgende Informationen zur Verfügung:
|
||||||
|
|
||||||
|
- Zeitpunkt,
|
||||||
|
- sendende beziehungsweise meldende Station,
|
||||||
|
- deren Locator,
|
||||||
|
- gemeldete Station,
|
||||||
|
- deren Locator,
|
||||||
|
- QRG,
|
||||||
|
- Meldungstext und
|
||||||
|
- globaler Worked-Status der gemeldeten Station.
|
||||||
|
|
||||||
|
Nicht jede vom Server übertragene Meldung enthält alle Felder. Ein leeres Locator- oder Nachrichtenfeld bedeutet deshalb nicht zwangsläufig einen Verarbeitungsfehler.
|
||||||
|
|
||||||
|
Diese Anzeige darf nicht mit dem [integrierten lokalen DX-Cluster-Server](de-DX-Cluster-Server) verwechselt werden. Der Tab zeigt empfangene ON4KST-Clusterinformationen. Der lokale Server erzeugt dagegen aus einer erkannten Richtungsgelegenheit einen Spot und gibt ihn an ein verbundenes Logprogramm weiter.
|
||||||
|
|
||||||
|
### QSO of the other
|
||||||
|
|
||||||
|
Der Tab **QSO of the other** zeigt gerichtete Chatnachrichten, bei denen weder Absender noch Empfänger die eigene Station sind. Öffentliche Nachrichten an `ALL` werden nicht aufgenommen.
|
||||||
|
|
||||||
|
Die Tabelle enthält:
|
||||||
|
|
||||||
|
| Spalte | Bedeutung |
|
||||||
|
|---|---|
|
||||||
|
| **Time** | Zeitpunkt der Chatnachricht |
|
||||||
|
| **Call TX** | Absender der Nachricht |
|
||||||
|
| **Last QRG TX** | zuletzt für den Absender bekannte QRG |
|
||||||
|
| **wkd TX?** | globaler Worked-Status des Absenders |
|
||||||
|
| **Call RX** | Empfänger der Nachricht |
|
||||||
|
| **Last QRG RX** | zuletzt für den Empfänger bekannte QRG |
|
||||||
|
| **wkd RX?** | globaler Worked-Status des Empfängers |
|
||||||
|
| **Message** | Inhalt der gerichteten Nachricht |
|
||||||
|
| **Category** | Chat-Kategorie der Nachricht |
|
||||||
|
|
||||||
|
Die beiden QRG-Spalten zeigen den zuletzt in KST4Contest bekannten Wert der jeweiligen Station. Das ist nicht zwangsläufig die Frequenz, auf der sich die beiden Stationen gerade verabreden. Die QRG kann aus einer früheren Nachricht stammen und sich inzwischen geändert haben.
|
||||||
|
|
||||||
|
Auch die Worked-Spalten sind bewusst bandunabhängig. Ein `X` bedeutet, dass das betreffende Basisrufzeichen auf mindestens einem Band gearbeitet wurde. Daraus folgt nicht, dass es auf der in der Tabelle sichtbaren oder vermuteten QRG bereits gearbeitet wurde.
|
||||||
|
|
||||||
|
Die Bezeichnung **QSO of the other** ist eine praktische Kurzform. Eine gerichtete Nachricht beweist weder, dass anschließend ein Funkkontakt zustande kam, noch dass beide Stationen tatsächlich auf derselben Frequenz arbeiten. Die Ansicht zeigt beobachtbare Koordination im Chat – nicht das Logbuch der anderen Stationen.
|
||||||
|
|
||||||
|
### Zusätzliches Monitorfenster
|
||||||
|
|
||||||
|
Dieselben DX-Cluster-Meldungen und gerichteten Nachrichten stehen weiterhin im separaten Fenster **Cluster & QSO of the other** zur Verfügung. Dort erscheinen die DX-Cluster-Tabelle oben und die Nachrichten zwischen anderen Stationen darunter.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Das separate Fenster und die Tabs verwenden dieselben zugrunde liegenden Listen. Eine Meldung wird dadurch nicht doppelt empfangen oder doppelt gespeichert. Es handelt sich lediglich um zwei Darstellungen derselben Daten.
|
||||||
|
|
||||||
|
Das Monitorfenster kann über **Windows → Hide cluster / stranger QSOs** ausgeblendet und mit **Show cluster / stranger QSOs** wieder eingeblendet werden. Wer den Platz nicht benötigt, kann das Fenster daher schließen oder minimieren, ohne auf die entsprechenden Tabs im Hauptfenster verzichten zu müssen.
|
||||||
|
|
||||||
|
Die vollständigen Texte abgeschnittener Nachrichten erscheinen als Tooltip. Erkannte Webadressen können wie in den übrigen Nachrichtentabellen angeklickt und im Standardbrowser geöffnet werden.
|
||||||
|
|
||||||
|
Die Ansichten helfen dabei, Aktivität und Koordination anderer Stationen zu erkennen. Bei hohem Chat-Aufkommen entsteht daraus allerdings schnell mehr Information als Erkenntnis. Das separate Fenster ist deshalb vor allem dann nützlich, wenn ein bestimmter Kommunikationsfluss gezielt beobachtet werden soll.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Stationskarte (ab v1.41)
|
## Stationskarte und Streckenanalyse (ab v1.41)
|
||||||
|
|
||||||
Eine interaktive OpenStreetMap-Karte zeigt die geografische Position aller aktiven Chatmember.
|
Eine lange Benutzerliste beantwortet zwei geografische Fragen nur unzureichend: Wo befinden sich die eingeloggten Stationen, und welche davon liegen ungefähr in der aktuellen Antennenrichtung? Die Stationskarte überträgt deshalb die bereits bekannten Locator-, Richtungs-, Band- und Worked-Informationen in eine interaktive Kartenansicht.
|
||||||
|
|
||||||
**Funktionen:**
|
Die Karte ist keine zweite, unabhängig verwaltete Stationsliste. Sie verwendet die aktuell durch die Filter der Benutzerliste sichtbaren Chatmember. Wird beispielsweise nach Entfernung, Richtung, Worked-Status oder einem bestimmten Band gefiltert, wirkt sich dies auch auf die dargestellten Stationen aus. In der Kopfzeile der Karte wird angezeigt, wie viele Stationen sichtbar sind und ob eine gefilterte Ansicht aktiv ist.
|
||||||
|
|
||||||
- Stationsmarker mit Rufzeichen-Labels, farblich nach Aktivität und Sked-Status
|

|
||||||
- **Antennen-Kegel** für die eigene Station
|
|
||||||
- **Verbindungslinie** zur aktuell ausgewählten Station
|
|
||||||
- **Maidenhead-Raster** (QRA-Locator-Gitter als Overlay)
|
|
||||||
- **Wegprofil-Diagramm**: Geländehöhen-Querschnitt zwischen eigener und ausgewählter Station, inklusive Fresnel-Zonen-Analyse und Horizonterkennung
|
|
||||||
- Mehrere Terrainquellen: **Copernicus GLO-30** (hochauflösendes DEM), **Open-Meteo API**, synthetischer Fallback und **Offline-DEM-Import** für den Betrieb ohne Internetverbindung
|
|
||||||
- Aircraft-Scatter-Weganalyse verknüpft mit den Geländedaten
|
|
||||||
|
|
||||||
Die Karte funktioniert in gepackten Umgebungen (AppImage, Flatpak) ohne Zugriff auf externe CDNs: Die Kartenkacheln werden über einen lokalen Tile-Proxy abgerufen, die Leaflet.js-Bibliothek ist in der Anwendung eingebettet.
|
### Welche Stationen werden dargestellt?
|
||||||
|
|
||||||
|
Für einen Kartenmarker benötigt KST4Contest einen brauchbaren sechsstelligen Locator. Chatmember ohne einen solchen Locator können in der Benutzerliste vorhanden sein, erscheinen aber nicht auf der Karte.
|
||||||
|
|
||||||
|
Mehrere aktive Chat-Einträge desselben Basisrufzeichens werden für die Kartenansicht zusammengefasst. Das verhindert, dass beispielsweise getrennte Logins in mehreren Chat-Kategorien mehrere Marker an derselben geografischen Position erzeugen. Als sichtbares Rufzeichen und für die Detailinformationen wird die zuletzt geeignete aktive Variante verwendet.
|
||||||
|
|
||||||
|
Die Beschriftung eines Markers kann zusätzlich enthalten:
|
||||||
|
|
||||||
|
- die für die Station erkannten aktiven Bänder,
|
||||||
|
- `B+`, wenn mindestens ein eigenes aktiviertes und noch nicht gearbeitetes Band angeboten wird.
|
||||||
|
|
||||||
|
Die Bandangaben verwenden dieselbe Herleitung wie die Bandspalten, der Filter **New bands** und der Priority Score. Aktuelle QRG-Erkennungen, Bandangaben im Namensfeld, Worked-Informationen und manuelle NOT-QRV-Markierungen werden daher auch in der Kartenansicht konsistent berücksichtigt.
|
||||||
|
|
||||||
|
### Bedeutung der Markerfarben
|
||||||
|
|
||||||
|
| Darstellung | Bedeutung |
|
||||||
|
|---|---|
|
||||||
|
| Blauer Rand | Station ohne eine der nachfolgenden besonderen Markierungen |
|
||||||
|
| Gelber Rand | Das Basisrufzeichen wurde bereits auf mindestens einem Band gearbeitet |
|
||||||
|
| Grün | Für die Station besteht eine aus gerichteten Chatnachrichten hergeleitete Richtungsgelegenheit |
|
||||||
|
| Orange | Aktuell ausgewählte Station |
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Auswahl und geografische Hilfen
|
||||||
|
|
||||||
|
Ein Klick auf einen einzelnen Stationsmarker:
|
||||||
|
|
||||||
|
1. wählt den dazugehörigen aktiven Chatmember aus,
|
||||||
|
2. scrollt die Benutzerliste zu diesem Eintrag,
|
||||||
|
3. aktualisiert den **Further Info**-Bereich und
|
||||||
|
4. bereitet das Rufzeichen wie bei einer Auswahl in der Benutzerliste als Nachrichtenziel vor.
|
||||||
|
|
||||||
|
Für die ausgewählte Station zeichnet KST4Contest eine Verbindungslinie von der eigenen Station zum Ziel. Der eingezeichnete Antennensektor verwendet:
|
||||||
|
|
||||||
|
- den aktuellen eigenen QTF,
|
||||||
|
- den konfigurierten Antennen-Öffnungswinkel und
|
||||||
|
- das konfigurierte Standard-Maximum-QRB.
|
||||||
|
|
||||||
|
Das Maidenhead-Raster passt seine Genauigkeit an die Zoomstufe und den sichtbaren Kartenausschnitt an. Es dient der räumlichen Orientierung; die Position eines Stationsmarkers wird aus dem sechsstelligen Locator abgeleitet und ist deshalb keine exakte GPS-Position.
|
||||||
|
|
||||||
|
### Strecken- und Geländeprofil
|
||||||
|
|
||||||
|
Nach Auswahl einer Station fordert KST4Contest ein Höhenprofil zwischen dem eigenen und dem fremden Locator an. Die aktive Online-Datenquelle ist die Open-Meteo Elevation API mit Geländedaten auf Basis von **Copernicus GLO-90**.
|
||||||
|
|
||||||
|
Die Online-Abfrage ist auf höchstens 100 gleichmäßig über die Strecke verteilte Höhenpunkte begrenzt. Eine 100 Kilometer lange Strecke wird damit grob im Abstand von etwa einem Kilometer abgetastet. Bei kürzeren Strecken wird der Abstand entsprechend kleiner, schmale Hindernisse können trotzdem zwischen zwei Abfragepunkten liegen.
|
||||||
|
|
||||||
|
Aus den Höhenpunkten berechnet KST4Contest unter anderem:
|
||||||
|
|
||||||
|
- das Geländeprofil,
|
||||||
|
- die geometrische Sichtlinie,
|
||||||
|
- die Erdkrümmung mit einem festen effektiven Erdradiusfaktor von `k = 4/3`,
|
||||||
|
- den geometrischen Radiohorizont beider Stationen,
|
||||||
|
- relevante Geländehorizonte,
|
||||||
|
- die erste Fresnel-Zone,
|
||||||
|
- die geringste Fresnel-Freiheit,
|
||||||
|
- den stärksten erkannten Eingriff in die Fresnel-Zone und
|
||||||
|
- eine grobe Einzelhindernis- beziehungsweise Knife-Edge-Abschätzung.
|
||||||
|
|
||||||
|
Die eingestellte eigene Antennenhöhe wird zur lokalen Geländehöhe addiert. Für die Gegenstation wird derzeit eine feste angenommene Antennenhöhe von 10 Metern über Grund verwendet.
|
||||||
|
|
||||||
|
Bewegst du die Maus über das Profil, wird der dazugehörige Abfragepunkt zusätzlich auf der Karte markiert. Dadurch lässt sich ein auffälliger Berg oder Geländeeinschnitt leichter einer geografischen Position zuordnen.
|
||||||
|
|
||||||
|
### Welche Frequenz wird für die Berechnung verwendet?
|
||||||
|
|
||||||
|
Die Frequenz beeinflusst insbesondere die Größe der Fresnel-Zone, die Freiraumdämpfung und das Link-Budget. KST4Contest versucht deshalb, für die ausgewählte Station eine passende Analysefrequenz zu bestimmen.
|
||||||
|
|
||||||
|
Vorrangig wird eine aktuell bekannte QRG auf einem eigenen aktivierten und für die Gegenstation nutzbaren Band verwendet. Fehlt eine geeignete QRG, wird aus den vorhandenen Bandinformationen ein automatisches Analyseband hergeleitet und dessen Standardfrequenz verwendet. Manuelle NOT-QRV-Markierungen werden dabei berücksichtigt.
|
||||||
|
|
||||||
|
Die tatsächlich verwendete Frequenz steht im Feld **Frequency** der Streckenanalyse. Sie sollte kontrolliert werden, wenn die automatische Zuordnung nicht zur vorgesehenen Funkverbindung passt. Eine Berechnung auf 144 MHz ist für eine geplante Verbindung auf 1296 MHz keine gleichwertige Näherung.
|
||||||
|
|
||||||
|
### Link-Budget und Tropo-Spalte
|
||||||
|
|
||||||
|
Zusätzlich zur geometrischen Bewertung erstellt KST4Contest eine vereinfachte Link-Budget-Abschätzung. Verwendet werden:
|
||||||
|
|
||||||
|
- die konfigurierte eigene Sendeleistung,
|
||||||
|
- der eigene Antennengewinn,
|
||||||
|
- die angenommene Sendeleistung der Gegenstation,
|
||||||
|
- der angenommene Antennengewinn der Gegenstation,
|
||||||
|
- eine frequenzabhängig geschätzte Speiseleitungsdämpfung,
|
||||||
|
- die Freiraumdämpfung und
|
||||||
|
- gegebenenfalls eine grobe zusätzliche Hindernisdämpfung.
|
||||||
|
|
||||||
|
Antennengewinne werden in `dBi` eingegeben. Ein in `dBd` bekannter Wert muss deshalb vor der Eingabe um `2,15 dB` erhöht werden.
|
||||||
|
|
||||||
|
Die Berechnung betrachtet beide Übertragungsrichtungen. Der daraus abgeleitete ungünstigere SSB-Wert wird als Tropo-Marge für die Station gespeichert und kann anschließend in der **Tropo**-Spalte, beim Sortieren und durch den Filter **Tropo >=0dB** verwendet werden.
|
||||||
|
|
||||||
|
Karte und Benutzerliste führen keine voneinander unabhängigen Berechnungen durch. Sie verwenden denselben Reachability-Service und denselben Ergebnisspeicher. Eine über die Karte oder mit **Calc selected** angestoßene Berechnung kann deshalb anschließend auch in der Benutzerliste erscheinen.
|
||||||
|
|
||||||
|
Es wird absichtlich keine automatische Online-Geländeabfrage für jeden sichtbaren Chatmember gestartet. Eine Berechnung erfolgt durch eine ausdrückliche Auswahl in der Karte oder über **Calc selected**. Das begrenzt API-Anfragen und verhindert, dass jede Tabellenaktualisierung eine neue Serie von Höhenabfragen auslöst.
|
||||||
|
|
||||||
|
### Pfadanalyse ausblenden
|
||||||
|
|
||||||
|
Das Geländeprofil und die ausführliche Analyse benötigen einen erheblichen Teil der Fensterhöhe. Werden sie gerade nicht gebraucht, können beide Bereiche gemeinsam mit **Hide path analysis** ausgeblendet werden. Die Karte nutzt den frei werdenden Platz unmittelbar.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Mit **Show path analysis** werden Profil und Detailwerte wieder eingeblendet. Das zuletzt berechnete Ergebnis bleibt erhalten. Die gewählte Sichtbarkeit wird in den Einstellungen gespeichert und beim nächsten Programmstart wiederhergestellt.
|
||||||
|
|
||||||
|
Der rechte Detailbereich ist über einen Divider in der Breite verstellbar. Er kann so weit verkleinert werden, dass mehr Platz für die Karte entsteht, ohne ein Rufzeichen mit üblicher Länge vollständig zu verdecken.
|
||||||
|
|
||||||
|
### Was sagt die Streckenanalyse nicht aus?
|
||||||
|
|
||||||
|
Die Auswertung ist ein geometrisches und rechnerisches Modell. Sie misst weder die tatsächliche Feldstärke noch die momentanen Ausbreitungsbedingungen.
|
||||||
|
|
||||||
|
Insbesondere kennt KST4Contest nicht:
|
||||||
|
|
||||||
|
- die wirkliche Antennenhöhe und den tatsächlichen Antennengewinn der Gegenstation,
|
||||||
|
- deren Sendeleistung und Speiseleitungsverluste,
|
||||||
|
- Gebäude, Bewuchs und andere Hindernisse, die im Höhenmodell nicht enthalten sind,
|
||||||
|
- lokale Störungen und Empfängereigenschaften,
|
||||||
|
- den aktuellen atmosphärischen K-Faktor,
|
||||||
|
- Inversionsschichten oder Ducting und
|
||||||
|
- die tatsächliche Antennenrichtung der Gegenstation.
|
||||||
|
|
||||||
|
Die unter **Mechanisms** genannten Ausbreitungswege sind eine allgemeine Einordnung anhand der Geländegeometrie. Eine Nennung von Aircraft Scatter bedeutet nicht, dass aktuelle Flugzeuge aus AirScout in die Geländeanalyse eingerechnet wurden.
|
||||||
|
|
||||||
|
Im Klartext: Ein freier Weg ist ein nützlicher positiver Hinweis. Ein geometrisch versperrter Weg bedeutet im VHF-, UHF- und Mikrowellenbereich aber nicht automatisch „unmöglich“. Ebenso garantiert ein positives Link-Budget kein QSO.
|
||||||
|
|
||||||
|
Bedienung: [Stationskarte in der Benutzeroberfläche](de-Benutzeroberflaeche#stationskarte)
|
||||||
|
|
||||||
|
Konfiguration: [Streckenanalyse und Link-Budget](de-Konfiguration#streckenanalyse-und-link-budget)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Optimierte Nachrichtenverarbeitung / 30.000-Nachrichten-Limit (ab v1.41)
|
## Begrenzte Nachrichtenspeicher (ab v1.41)
|
||||||
|
|
||||||
|
Chat- und DX-Cluster-Meldungen werden während des Betriebs im Arbeitsspeicher gehalten. Damit ein mehrtägiger Contest nicht zu einem unbegrenzt wachsenden Speicherverbrauch und immer langsameren Tabellen führt, besitzen beide Speicher feste Grenzen:
|
||||||
|
|
||||||
|
| Nachrichtenspeicher | Maximale Größe | Größe nach dem automatischen Aufräumen |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Chatnachrichten | 30.000 | 25.000 |
|
||||||
|
| DX-Cluster-Meldungen | 10.000 | 8.000 |
|
||||||
|
|
||||||
|
Wird die jeweilige Maximalgröße überschritten, entfernt KST4Contest die ältesten Einträge am Ende der Liste. Neue Nachrichten bleiben erhalten und werden weiterhin zuerst angezeigt.
|
||||||
|
|
||||||
|
Die öffentlichen Nachrichten, PMs, Stationsinformationen und **QSO of the other** besitzen keine voneinander getrennten 30.000-Einträge-Speicher. Sie sind gefilterte Ansichten derselben globalen Chatnachrichtenliste. Auch die DX-Cluster-Tabelle im Hauptfenster und die Tabelle im separaten Monitorfenster verwenden denselben Cluster-Speicher.
|
||||||
|
|
||||||
|
Die Nachrichten werden nicht dauerhaft gespeichert. Nach einem Neustart beginnen die Ansichten wieder mit leeren Listen.
|
||||||
|
|
||||||
Die internen Chat- und Nachrichtentabellen sind auf **30.000 Einträge** begrenzt. Ältere Nachrichten werden automatisch verworfen, sobald das Limit erreicht wird. Damit bleiben Speicherverbrauch und Darstellungsperformance auch bei mehrtägigen Contest-Betrieb stabil.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,65 @@ Der Wert sollte zum eigenen Stationsaufbau und zum vorgesehenen Contestbetrieb p
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Streckenanalyse und Link-Budget
|
||||||
|
|
||||||
|
Die Streckenanalyse der Stationskarte verwendet einige Angaben aus der Stationskonfiguration. Diese Werte beschreiben das eigene Stationssetup und – soweit keine individuellen Daten der Gegenstation vorliegen – ein angenommenes Setup der Gegenstation.
|
||||||
|
|
||||||
|
Folgende Einstellungen werden berücksichtigt:
|
||||||
|
|
||||||
|
- **Own antenna height AGL [m]** gibt die Höhe der eigenen Antenne über dem lokalen Gelände an. Die Angabe bezieht sich auf *Above Ground Level* und nicht auf die Höhe über dem Meeresspiegel. KST4Contest addiert diesen Wert zur Geländehöhe am eigenen Standort.
|
||||||
|
- **Own TX power [W]** gibt die verwendete Sendeleistung der eigenen Station in Watt an.
|
||||||
|
- **Own ant. gain [dBi]** gibt den Antennengewinn der eigenen Station in dBi an.
|
||||||
|
- **DX OM TX power [W]** gibt die für die Gegenstation angenommene Sendeleistung in Watt an.
|
||||||
|
- **DX OM ant. gain [dBi]** gibt den für die Gegenstation angenommenen Antennengewinn in dBi an.
|
||||||
|
|
||||||
|
Für die Antennenhöhe der Gegenstation verwendet KST4Contest derzeit einen festen Wert von 10 m über dem lokalen Gelände. Die Leistungs- und Antennendaten der Gegenstation sind globale Annahmen. Sie ersetzen keine individuell bekannten Stationsdaten, ermöglichen aber eine einheitliche Abschätzung, wenn keine genaueren Informationen vorliegen.
|
||||||
|
|
||||||
|
Die Antennengewinne müssen in dBi angegeben werden. Falls ein Wert in dBd vorliegt, kann er näherungsweise wie folgt umgerechnet werden:
|
||||||
|
|
||||||
|
`dBi = dBd + 2.15`
|
||||||
|
|
||||||
|
Auch die aktuelle Antennenrichtung, die konfigurierte Strahlbreite, das maximale QRB und die für die eigene Station aktivierten Bänder beeinflussen die Darstellung oder Auswertung auf der Stationskarte. Die aktuelle QTF und die Strahlbreite bestimmen beispielsweise den eingezeichneten Antennensektor und die Hervorhebung von Stationen innerhalb dieses Bereichs.
|
||||||
|
|
||||||
|
Für die Berücksichtigung der Erdkrümmung verwendet die Streckenanalyse einen festen Faktor von `k = 4/3` für den effektiven Erdradius. Das ist eine übliche Näherung für eine durchschnittliche troposphärische Refraktion. Tatsächliche Ausbreitungsbedingungen können davon deutlich abweichen.
|
||||||
|
|
||||||
|
Das Link-Budget berücksichtigt unter anderem:
|
||||||
|
|
||||||
|
- die Entfernung zwischen beiden Stationen,
|
||||||
|
- die verwendete Frequenz,
|
||||||
|
- die konfigurierte Sendeleistung,
|
||||||
|
- die Antennengewinne,
|
||||||
|
- geschätzte Speiseleitungsverluste,
|
||||||
|
- den Freiraumverlust sowie
|
||||||
|
- eine grobe Zusatzdämpfung durch Hindernisse im Streckenprofil.
|
||||||
|
|
||||||
|
Die daraus berechnete Empfangsleistung und SSB- beziehungsweise CW-Marge sind technische Abschätzungen. Sie sollen dabei helfen, mögliche Verbindungen einzuordnen. Sie sind keine vollständige Feldstärkeprognose und können insbesondere aktuelle Wetterbedingungen, lokale Abschattungen, Mehrwegeausbreitung oder andere nicht bekannte Stationsparameter nicht vollständig berücksichtigen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Streckenanalyse und Link-Budget
|
||||||
|
|
||||||
|
Die Stationskarte verwendet mehrere Werte aus dem Reiter **Station**, um das Geländeprofil und das Link-Budget zur ausgewählten Gegenstation zu berechnen.
|
||||||
|
|
||||||
|
| Einstellung | Verwendung |
|
||||||
|
|---|---|
|
||||||
|
| **Own antenna height AGL** | Höhe der eigenen Antenne über dem lokalen Gelände in Metern |
|
||||||
|
| **Own TX power W** | Eigene Sendeleistung in Watt |
|
||||||
|
| **Own ant. gain dBi** | Gewinn der eigenen Antenne in dBi |
|
||||||
|
| **DX OM TX power W** | Angenommene Sendeleistung der Gegenstation in Watt |
|
||||||
|
| **DX OM ant. gain dBi** | Angenommener Antennengewinn der Gegenstation in dBi |
|
||||||
|
|
||||||
|
**AGL** bedeutet „above ground level“. Trage hier nicht die Höhe über dem Meeresspiegel ein. Die Geländehöhe am eigenen Standort stammt bereits aus dem abgerufenen Höhenprofil; die konfigurierte Antennenhöhe wird zu diesem Wert addiert.
|
||||||
|
|
||||||
|
Für die Antennenhöhe der Gegenstation verwendet KST4Contest derzeit einen festen Standardwert von 10 Metern über Grund. Eine stationsbezogene Antennenhöhe wird im ON4KST-Chat nicht übertragen.
|
||||||
|
|
||||||
|
Antennengewinne müssen in `dBi` eingetragen werden. Liegt ein Wert in `dBd` vor, gilt:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dBi = dBd + 2,15 dB
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Server-Einstellungen (ab v1.31)
|
## Server-Einstellungen (ab v1.31)
|
||||||
|
|
||||||
Der Chat-Server-DNS und -Port sind in den Preferences konfigurierbar:
|
Der Chat-Server-DNS und -Port sind in den Preferences konfigurierbar:
|
||||||
|
|||||||
@@ -509,13 +509,83 @@ Automatic CQ messages in the public channel at a configurable interval. Recommen
|
|||||||
|
|
||||||
## Simplelogfile
|
## Simplelogfile
|
||||||
|
|
||||||
File-based log evaluation using regex. Details: [Log Synchronisation](Log-Sync#method-1-universal-file-based-callsign-interpreter-simplelogfile).
|
File-based log evaluation using regex. Details: [Log Synchronisation](en-Log-Sync#method-1-universal-file-based-callsign-interpreter-simplelogfile).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Cluster & QSO of Others
|
## Global Message Views
|
||||||
|
|
||||||
A separate window showing the QSO flow between other stations. Particularly interesting during quieter night-time hours of a contest. This window can be minimised when not needed. Future plan: filtering to stations in your selected QTF.
|
Most message tables in KST4Contest are deliberately tied either to the local station or to the station currently selected in the user list. Some message streams must, however, remain visible independently of that selection.
|
||||||
|
|
||||||
|
KST4Contest therefore provides three global message tabs below the main user list:
|
||||||
|
|
||||||
|
| Tab | Content |
|
||||||
|
|---|---|
|
||||||
|
| **Public messages** | Public chat messages, including CQ calls and beacon messages |
|
||||||
|
| **DXCluster messages** | DX cluster messages delivered by the ON4KST server |
|
||||||
|
| **QSO of the other** | Directed chat messages between chat logins other than the local station |
|
||||||
|
|
||||||
|
**Public messages** is selected by default. Changing the selected station does not affect any of these three views.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### DXCluster messages
|
||||||
|
|
||||||
|
The DX cluster table shows cluster messages received through the ON4KST connection. Depending on the information contained in the source message, the table displays:
|
||||||
|
|
||||||
|
- the time,
|
||||||
|
- the reporting station and its locator,
|
||||||
|
- the reported station and its locator,
|
||||||
|
- the QRG,
|
||||||
|
- the message text, and
|
||||||
|
- the global Worked state of the reported station.
|
||||||
|
|
||||||
|
An empty locator or another empty field does not necessarily indicate a processing error. The corresponding information may simply be absent from the source message.
|
||||||
|
|
||||||
|
This view must not be confused with the [built-in DX Cluster server](en-DX-Cluster-Server). The built-in server sends derived direction spots to connected logging software. The **DXCluster messages** tab displays cluster traffic received from ON4KST.
|
||||||
|
|
||||||
|
### QSO of the other
|
||||||
|
|
||||||
|
The **QSO of the other** table displays directed chat messages for which neither the sender nor the receiver is the local station. Messages addressed to `ALL` are not included.
|
||||||
|
|
||||||
|
The table contains the following columns:
|
||||||
|
|
||||||
|
| Column | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| **Time** | Time of the chat message |
|
||||||
|
| **Call TX** | Complete callsign of the sender |
|
||||||
|
| **Last QRG TX** | Most recently detected QRG assigned to the sender |
|
||||||
|
| **wkd TX?** | Global Worked state of the sender |
|
||||||
|
| **Call RX** | Complete callsign of the receiver |
|
||||||
|
| **Last QRG RX** | Most recently detected QRG assigned to the receiver |
|
||||||
|
| **wkd RX?** | Global Worked state of the receiver |
|
||||||
|
| **Message** | Message text |
|
||||||
|
| **Category** | Chat category in which the message was received |
|
||||||
|
|
||||||
|
The QRG columns are not a historical record of the frequency used for the displayed message. They show the latest QRG currently known for the respective chat member. The value may originate from another message and may change when a newer QRG is detected.
|
||||||
|
|
||||||
|
The two Worked columns show the global callsign state. They do not indicate whether the station has already been worked on the QRG or band shown next to it.
|
||||||
|
|
||||||
|
The expression “QSO of the other” is used as a compact user-interface label. A directed chat message does not prove that an actual radio QSO has taken place. It may equally be a sked request, a frequency exchange or another private message between two chat logins.
|
||||||
|
|
||||||
|
### Separate monitor window
|
||||||
|
|
||||||
|
The DX cluster and QSO-of-the-other tables are additionally available in a separate monitor window. It places the DX cluster table above the directed messages between other stations.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The tabs and the monitor window use the same underlying message stores. Opening the separate window does not create another connection, receive the messages a second time or maintain an independent history.
|
||||||
|
|
||||||
|
The window can be hidden or restored through:
|
||||||
|
|
||||||
|
**Windows → Hide cluster / stranger QSOs**
|
||||||
|
|
||||||
|
or:
|
||||||
|
|
||||||
|
**Windows → Show cluster / stranger QSOs**
|
||||||
|
|
||||||
|
The additional window is useful when these message streams should remain visible on a second monitor or while another part of the main window is being used. During periods with heavy chat traffic, the global tabs are usually more compact.
|
||||||
|
|
||||||
|
When a table cell cannot display its complete message, moving the mouse over the cell shows the full text in a tooltip. Web links beginning with `http://`, `https://` or `www.` can be opened in the system browser.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -537,9 +607,20 @@ The map works in packaged environments (AppImage, Flatpak) without internet acce
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Optimised Message Handling / 30,000 Message Limit (from v1.41)
|
## Bounded Message Stores (from v1.41)
|
||||||
|
|
||||||
The internal chat and message tables are capped at **30,000 entries**. Older messages are automatically discarded when the limit is reached. This keeps memory usage and rendering performance stable during multi-day contest operations.
|
KST4Contest keeps the received chat and DX cluster messages in two separate bounded memory stores. The limits apply to the stored messages, not independently to every table displaying them.
|
||||||
|
|
||||||
|
| Message store | Maximum size | Size after automatic cleanup |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Chat messages | 30,000 | 25,000 |
|
||||||
|
| DX cluster messages | 10,000 | 8,000 |
|
||||||
|
|
||||||
|
When a store exceeds its maximum size, the oldest entries are removed until the cleanup size is reached. Newer messages remain available and are displayed first.
|
||||||
|
|
||||||
|
The public-message table, private-message views, station-related message views and **QSO of the other** table are filtered views of the same chat-message store. They do not each retain another 30,000 messages. The DX cluster tab and the separate monitor window likewise share the same DX cluster store.
|
||||||
|
|
||||||
|
These message stores are held in memory and are not written to the internal database. After restarting KST4Contest, they are rebuilt from messages received during the new session.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -158,15 +158,58 @@ Calculation and limitations: [Priority Score and Priority List](en-Features#prio
|
|||||||
|
|
||||||
## Cluster & QSO of Others
|
## Cluster & QSO of Others
|
||||||
|
|
||||||
Separate window (can be minimised). Shows the communication flow between other stations – interesting during quieter contest periods.
|
## Global Message Tabs and Monitor Window
|
||||||
|
|
||||||
|
Three global message tabs are located below the main user list. Unlike the **Further Info** panel, their contents do not depend on the station currently selected.
|
||||||
|
|
||||||
|
| Tab | Displayed messages |
|
||||||
|
|---|---|
|
||||||
|
| **Public messages** | All public chat messages, including CQ calls and beacons |
|
||||||
|
| **DXCluster messages** | DX cluster messages received from the ON4KST server |
|
||||||
|
| **QSO of the other** | Directed messages between chat logins other than the local station |
|
||||||
|
|
||||||
|
The **Public messages** tab is selected by default.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The **DXCluster messages** table contains the time, reporting and reported stations, locators, QRG, message text and global Worked state where these values are available in the received message.
|
||||||
|
|
||||||
|
The **QSO of the other** table contains:
|
||||||
|
|
||||||
|
- the complete sender and receiver callsigns,
|
||||||
|
- the latest QRG currently known for each station,
|
||||||
|
- the global Worked state of each station,
|
||||||
|
- the message text, and
|
||||||
|
- the chat category.
|
||||||
|
|
||||||
|
The displayed QRG is not necessarily the frequency on which the stations intend to make a contact. It is the latest QRG currently associated with the respective chat member. The Worked state is global and not specific to the displayed QRG or band.
|
||||||
|
|
||||||
|
A directed chat message in this table does not prove that a radio QSO has taken place. The table also contains sked requests, frequency exchanges and other directed messages between third-party chat logins.
|
||||||
|
|
||||||
|
### Separate monitor window
|
||||||
|
|
||||||
|
The DX cluster and QSO-of-the-other tables can also be displayed together in a separate window.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The separate window and the tabs use the same underlying messages. Hiding the window does not stop message processing or remove messages from the tabs.
|
||||||
|
|
||||||
|
Use **Windows → Hide cluster / stranger QSOs** to hide the window and **Windows → Show cluster / stranger QSOs** to restore it.
|
||||||
|
|
||||||
|
If a message is too long for its table cell, moving the mouse over the cell displays the complete text in a tooltip. Links beginning with `http://`, `https://` or `www.` can be opened in the system browser.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Menu
|
## Menu
|
||||||
|
|
||||||
### Window
|
### Windows
|
||||||
- **Use Dark Mode** (from v1.26): Toggle dark colour scheme on/off.
|
|
||||||
|
|
||||||
|
- **Hide cluster / stranger QSOs** hides the separate monitor window for DX cluster messages and directed messages between other stations.
|
||||||
|
- **Show cluster / stranger QSOs** restores the monitor window.
|
||||||
|
- **hide options** hides the settings window.
|
||||||
|
- **show options** restores the settings window.
|
||||||
|
- **Use dark mode design** activates the dark colour scheme.
|
||||||
|
- **Use default mode design** restores the default colour scheme.
|
||||||
---
|
---
|
||||||
|
|
||||||
## Window Sizes and Dividers
|
## Window Sizes and Dividers
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
+128
-11
@@ -5,13 +5,19 @@ import java.net.DatagramPacket;
|
|||||||
import java.net.DatagramSocket;
|
import java.net.DatagramSocket;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import kst4contest.model.Band;
|
||||||
import kst4contest.model.ChatMember;
|
import kst4contest.model.ChatMember;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends periodical path requests and an AirScout watchlist for the currently
|
* Sends periodical path requests and an AirScout watchlist for the currently
|
||||||
* active ON4KST stations.
|
* active ON4KST stations.
|
||||||
@@ -24,8 +30,17 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
|||||||
|
|
||||||
private static final String BROADCAST_ADDRESS = "255.255.255.255";
|
private static final String BROADCAST_ADDRESS = "255.255.255.255";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private final ChatController client;
|
private final ChatController client;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ASWATCHLIST is sent as one common list. Remember one syntactically valid
|
||||||
|
* AirScout band value so an empty list can still be sent on a later cycle
|
||||||
|
* to remove stations which are no longer active.
|
||||||
|
*/
|
||||||
|
private String lastWatchListBandValue;
|
||||||
|
|
||||||
public AirScoutPeriodicalAPReflectionInquirerTask(
|
public AirScoutPeriodicalAPReflectionInquirerTask(
|
||||||
ChatController client
|
ChatController client
|
||||||
) {
|
) {
|
||||||
@@ -50,9 +65,6 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
|||||||
client.getChatPreferences().getAirScout_asClientNameString();
|
client.getChatPreferences().getAirScout_asClientNameString();
|
||||||
String serverIdentifier =
|
String serverIdentifier =
|
||||||
client.getChatPreferences().getAirScout_asServerNameString();
|
client.getChatPreferences().getAirScout_asServerNameString();
|
||||||
String bandValue =
|
|
||||||
client.getChatPreferences().getAirScout_asBandString();
|
|
||||||
|
|
||||||
String ownCallSign = normalizeOwnCallSign(
|
String ownCallSign = normalizeOwnCallSign(
|
||||||
client.getChatPreferences().getStn_loginCallSign()
|
client.getChatPreferences().getStn_loginCallSign()
|
||||||
);
|
);
|
||||||
@@ -79,14 +91,10 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
|||||||
+ "\" \"" + serverIdentifier + "\" ";
|
+ "\" \"" + serverIdentifier + "\" ";
|
||||||
|
|
||||||
String ownStation = ownCallSign + "," + ownLocator;
|
String ownStation = ownCallSign + "," + ownLocator;
|
||||||
StringBuilder watchListMessage = new StringBuilder(
|
|
||||||
watchListPrefix
|
|
||||||
+ bandValue
|
|
||||||
+ ","
|
|
||||||
+ ownStation
|
|
||||||
);
|
|
||||||
|
|
||||||
List<ChatMember> activeMembers = client.snapshotChatMembers();
|
List<ChatMember> activeMembers = client.snapshotChatMembers();
|
||||||
|
List<String> watchListTargets = new ArrayList<>();
|
||||||
|
Set<String> processedCallsigns = new LinkedHashSet<>();
|
||||||
|
String watchListBandValue = null;
|
||||||
int port = client.getChatPreferences()
|
int port = client.getChatPreferences()
|
||||||
.getAirScout_asCommunicationPort();
|
.getAirScout_asCommunicationPort();
|
||||||
|
|
||||||
@@ -102,11 +110,38 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (member.getQrb()
|
if (member.getQrb() == null
|
||||||
|
|| member.getQrb()
|
||||||
>= client.getChatPreferences().getStn_maxQRBDefault()) {
|
>= client.getChatPreferences().getStn_maxQRBDefault()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String callsignKey = member.getCallSignRaw();
|
||||||
|
if (callsignKey == null || callsignKey.isBlank()) {
|
||||||
|
callsignKey = member.getCallSign();
|
||||||
|
}
|
||||||
|
if (callsignKey == null
|
||||||
|
|| !processedCallsigns.add(
|
||||||
|
callsignKey.trim().toUpperCase(Locale.ROOT)
|
||||||
|
)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The resolver may deliberately return an exact QRG. AirScout must
|
||||||
|
* only see a canonical protocol band value such as 4320000.
|
||||||
|
*/
|
||||||
|
String bandValue = canonicalizeAirScoutBandValue(
|
||||||
|
client.resolveAirScoutBandValue(member)
|
||||||
|
);
|
||||||
|
if (bandValue == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (watchListBandValue == null) {
|
||||||
|
watchListBandValue = bandValue;
|
||||||
|
}
|
||||||
|
|
||||||
String targetStation =
|
String targetStation =
|
||||||
member.getCallSign() + "," + member.getQra();
|
member.getCallSign() + "," + member.getQra();
|
||||||
|
|
||||||
@@ -126,6 +161,30 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
|||||||
pathQuery
|
pathQuery
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watchListTargets.add(targetStation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AirScout keeps one watchlist per client/server pair. Do not send
|
||||||
|
* separate lists for the individual station bands because a later
|
||||||
|
* list would replace stations from an earlier one.
|
||||||
|
*
|
||||||
|
* If there are no targets in this cycle, reuse the last valid band
|
||||||
|
* token and send an empty list so AirScout can clear stale entries.
|
||||||
|
*/
|
||||||
|
if (watchListBandValue == null) {
|
||||||
|
watchListBandValue = lastWatchListBandValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (watchListBandValue != null) {
|
||||||
|
StringBuilder watchListMessage = new StringBuilder(
|
||||||
|
watchListPrefix
|
||||||
|
+ watchListBandValue
|
||||||
|
+ ","
|
||||||
|
+ ownStation
|
||||||
|
);
|
||||||
|
|
||||||
|
for (String targetStation : watchListTargets) {
|
||||||
watchListMessage
|
watchListMessage
|
||||||
.append(",")
|
.append(",")
|
||||||
.append(targetStation);
|
.append(targetStation);
|
||||||
@@ -139,6 +198,10 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
|||||||
port,
|
port,
|
||||||
watchListMessage.toString()
|
watchListMessage.toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
lastWatchListBandValue = watchListBandValue;
|
||||||
|
}
|
||||||
|
|
||||||
} catch (IOException exception) {
|
} catch (IOException exception) {
|
||||||
LOGGER.log(
|
LOGGER.log(
|
||||||
Level.WARNING,
|
Level.WARNING,
|
||||||
@@ -148,6 +211,60 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a frequency-like value returned by the station resolver into the
|
||||||
|
* canonical band token expected by the AirScout UDP protocol.
|
||||||
|
*
|
||||||
|
* <p>The internal resolver may keep an exact working frequency for path
|
||||||
|
* analysis. This method removes that precision only at the AirScout protocol
|
||||||
|
* boundary. For example, {@code 4321740} is sent to AirScout as
|
||||||
|
* {@code 4320000}.</p>
|
||||||
|
*
|
||||||
|
* @param resolvedValue frequency-like AirScout value produced by the resolver
|
||||||
|
* @return canonical AirScout band value, or {@code null} if unsupported
|
||||||
|
*/
|
||||||
|
private String canonicalizeAirScoutBandValue(String resolvedValue) {
|
||||||
|
if (resolvedValue == null || resolvedValue.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalizedValue = resolvedValue.trim();
|
||||||
|
|
||||||
|
if ("off".equalsIgnoreCase(normalizedValue)
|
||||||
|
|| "auto".equalsIgnoreCase(normalizedValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final long numericValue;
|
||||||
|
try {
|
||||||
|
numericValue = Long.parseLong(normalizedValue);
|
||||||
|
} catch (NumberFormatException exception) {
|
||||||
|
LOGGER.log(
|
||||||
|
Level.WARNING,
|
||||||
|
"Unsupported AirScout band value: " + resolvedValue,
|
||||||
|
exception
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
double frequencyMHz = numericValue / 10_000.0;
|
||||||
|
Band band = Band.fromFrequency(frequencyMHz);
|
||||||
|
|
||||||
|
if (band == null) {
|
||||||
|
LOGGER.warning(
|
||||||
|
"AirScout query skipped because frequency "
|
||||||
|
+ frequencyMHz
|
||||||
|
+ " MHz does not belong to a supported band."
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return band.getPrefix() + "0000";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the ON4KST login suffix because AirScout expects the actual
|
* Removes the ON4KST login suffix because AirScout expects the actual
|
||||||
* station callsign, for example 9A1W instead of 9A1W-2.
|
* station callsign, for example 9A1W instead of 9A1W-2.
|
||||||
|
|||||||
@@ -450,8 +450,15 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
chatPreferences.getAirScout_asClientNameString();
|
chatPreferences.getAirScout_asClientNameString();
|
||||||
String serverIdentifier =
|
String serverIdentifier =
|
||||||
chatPreferences.getAirScout_asServerNameString();
|
chatPreferences.getAirScout_asServerNameString();
|
||||||
String bandValue =
|
String bandValue = resolveAirScoutBandValue(remoteChatMember);
|
||||||
chatPreferences.getAirScout_asBandString();
|
if (bandValue == null) {
|
||||||
|
System.out.println(
|
||||||
|
"[AirScout, info]: Show-path request ignored because no "
|
||||||
|
+ "usable propagation frequency could be resolved."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int port =
|
int port =
|
||||||
chatPreferences.getAirScout_asCommunicationPort();
|
chatPreferences.getAirScout_asCommunicationPort();
|
||||||
|
|
||||||
@@ -498,6 +505,30 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the AirScout protocol frequency for one station. In automatic mode
|
||||||
|
* this uses the same station-specific resolution as the internal path analysis;
|
||||||
|
* manual mode keeps the configured forced value for compatibility.
|
||||||
|
*
|
||||||
|
* @param remoteChatMember target station
|
||||||
|
* @return AirScout frequency in 100-Hz units, or {@code null} if auto mode has
|
||||||
|
* no safe result
|
||||||
|
*/
|
||||||
|
public String resolveAirScoutBandValue(ChatMember remoteChatMember) {
|
||||||
|
if (!chatPreferences.isAirScout_autoBandSelectionEnabled()) {
|
||||||
|
return chatPreferences.getAirScout_asBandString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reachabilityService == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolution = reachabilityService
|
||||||
|
.resolveAutomaticPropagationFrequency(remoteChatMember);
|
||||||
|
|
||||||
|
return resolution == null ? null : resolution.getAirScoutBandValue();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* starts the calculation scheduler for scores / priorities of skeds to be made
|
* starts the calculation scheduler for scores / priorities of skeds to be made
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package kst4contest.controller;
|
package kst4contest.controller;
|
||||||
import kst4contest.logic.BandOpportunityResolver;
|
import kst4contest.logic.BandOpportunityResolver;
|
||||||
import kst4contest.view.map.MapCallsignRawSnapshot;
|
import kst4contest.view.map.MapCallsignRawSnapshot;
|
||||||
|
import kst4contest.logic.PropagationFrequencyResolver;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -12,9 +13,7 @@ import java.util.function.Consumer;
|
|||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import kst4contest.locatorUtils.Location;
|
import kst4contest.locatorUtils.Location;
|
||||||
import kst4contest.model.Band;
|
import kst4contest.model.Band;
|
||||||
import kst4contest.model.ChatCategory;
|
|
||||||
import kst4contest.model.ChatMember;
|
import kst4contest.model.ChatMember;
|
||||||
import kst4contest.model.ChatPreferences;
|
|
||||||
import kst4contest.view.map.GeometryOnlyPathAnalysisService;
|
import kst4contest.view.map.GeometryOnlyPathAnalysisService;
|
||||||
|
|
||||||
import kst4contest.view.map.OpenMeteoTerrainProfileProvider;
|
import kst4contest.view.map.OpenMeteoTerrainProfileProvider;
|
||||||
@@ -22,7 +21,6 @@ import kst4contest.view.map.PathAnalysisRequest;
|
|||||||
import kst4contest.view.map.PathAnalysisResult;
|
import kst4contest.view.map.PathAnalysisResult;
|
||||||
import kst4contest.view.map.PathAnalysisService;
|
import kst4contest.view.map.PathAnalysisService;
|
||||||
import kst4contest.view.map.PathGeometryUtils;
|
import kst4contest.view.map.PathGeometryUtils;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -139,9 +137,11 @@ public final class ReachabilityService {
|
|||||||
* @param member best matching ChatMember, may be null when only a map snapshot exists
|
* @param member best matching ChatMember, may be null when only a map snapshot exists
|
||||||
* @param selectedSnapshot selected map snapshot
|
* @param selectedSnapshot selected map snapshot
|
||||||
* @param fxCallback callback executed on the JavaFX thread
|
* @param fxCallback callback executed on the JavaFX thread
|
||||||
|
* @param requestedBandOverride operator-selected band, or null for automatic resolution
|
||||||
*/
|
*/
|
||||||
public void requestPathAnalysisForMap(ChatMember member,
|
public void requestPathAnalysisForMap(ChatMember member,
|
||||||
MapCallsignRawSnapshot selectedSnapshot,
|
MapCallsignRawSnapshot selectedSnapshot,
|
||||||
|
Band requestedBandOverride,
|
||||||
Consumer<PathAnalysisResult> fxCallback) {
|
Consumer<PathAnalysisResult> fxCallback) {
|
||||||
|
|
||||||
String ownLocator6 = normalizeLocator6(chatController.getChatPreferences().getStn_loginLocatorMainCat());
|
String ownLocator6 = normalizeLocator6(chatController.getChatPreferences().getStn_loginLocatorMainCat());
|
||||||
@@ -165,22 +165,23 @@ public final class ReachabilityService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
double analysisFrequencyMHz = PathGeometryUtils.resolveAnalysisFrequencyMHz(
|
Band analysisBand;
|
||||||
selectedSnapshot.lastKnownFrequenciesByBand()
|
double analysisFrequencyMHz;
|
||||||
);
|
|
||||||
|
|
||||||
Band analysisBand = Band.fromFrequency(analysisFrequencyMHz);
|
if (requestedBandOverride != null) {
|
||||||
if (analysisBand != null && !isUsableAutomaticBand(member, analysisBand)) {
|
/*
|
||||||
analysisFrequencyMHz = Double.NaN;
|
* An explicit operator selection has priority over automatic propagation
|
||||||
analysisBand = null;
|
* resolution. Exact recent QRG information on that band is still used
|
||||||
}
|
* when available; otherwise the band's default analysis frequency is used.
|
||||||
|
*/
|
||||||
|
analysisBand = requestedBandOverride;
|
||||||
|
analysisFrequencyMHz =
|
||||||
|
resolveAnalysisFrequencyForBand(member, analysisBand);
|
||||||
|
} else {
|
||||||
|
PropagationFrequencyResolver.Resolution frequencyResolution =
|
||||||
|
resolveAutomaticPropagationFrequency(member);
|
||||||
|
|
||||||
if (!Double.isFinite(analysisFrequencyMHz)
|
if (frequencyResolution == null) {
|
||||||
|| analysisFrequencyMHz <= 0.0
|
|
||||||
|| analysisBand == null) {
|
|
||||||
|
|
||||||
Band fallbackBand = resolveAutoBand(member);
|
|
||||||
if (fallbackBand == null) {
|
|
||||||
dispatchFxCallback(
|
dispatchFxCallback(
|
||||||
fxCallback,
|
fxCallback,
|
||||||
PathAnalysisResult.waitingForUsableBand(
|
PathAnalysisResult.waitingForUsableBand(
|
||||||
@@ -192,8 +193,9 @@ public final class ReachabilityService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
analysisBand = fallbackBand;
|
analysisBand = frequencyResolution.getBand();
|
||||||
analysisFrequencyMHz = resolveAnalysisFrequencyForBand(member, fallbackBand);
|
analysisFrequencyMHz =
|
||||||
|
frequencyResolution.getAnalysisFrequencyMHz();
|
||||||
}
|
}
|
||||||
|
|
||||||
PathAnalysisRequest request = buildRequest(
|
PathAnalysisRequest request = buildRequest(
|
||||||
@@ -236,72 +238,32 @@ public final class ReachabilityService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the auto reachability band.
|
* Resolves the auto reachability band through the shared propagation
|
||||||
*
|
* frequency selection used by AirScout and path analysis.
|
||||||
* <ol>
|
|
||||||
* <li>Use the lowest band detected in this session.</li>
|
|
||||||
* <li>If no session band exists and the station is in the microwave category, use 1296 MHz.</li>
|
|
||||||
* <li>Otherwise use 144 MHz.</li>
|
|
||||||
* </ol>
|
|
||||||
*
|
*
|
||||||
* @param member member to inspect
|
* @param member member to inspect
|
||||||
* @return resolved band
|
* @return resolved band
|
||||||
*/
|
*/
|
||||||
public Band resolveAutoBand(ChatMember member) {
|
public Band resolveAutoBand(ChatMember member) {
|
||||||
EnumSet<Band> enabledBands = getEnabledStationBands();
|
PropagationFrequencyResolver.Resolution resolution =
|
||||||
if (enabledBands.isEmpty()) {
|
resolveAutomaticPropagationFrequency(member);
|
||||||
return null;
|
return resolution == null ? null : resolution.getBand();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ChatMember> variants = resolveCallsignVariants(member);
|
/**
|
||||||
BandOpportunityResolver.Resolution resolution =
|
* Resolves one automatic band and exact analysis frequency for a station.
|
||||||
BandOpportunityResolver.resolve(variants, System.currentTimeMillis());
|
*
|
||||||
|
* @param member any active category variant of the target station
|
||||||
EnumSet<Band> availableOfferedBands = resolution.getAvailableBands();
|
* @return shared propagation resolution, or {@code null} for unsupported data
|
||||||
availableOfferedBands.retainAll(enabledBands);
|
*/
|
||||||
|
public PropagationFrequencyResolver.Resolution resolveAutomaticPropagationFrequency(
|
||||||
if (!availableOfferedBands.isEmpty()) {
|
ChatMember member
|
||||||
return availableOfferedBands.stream()
|
) {
|
||||||
.min(Comparator.comparingDouble(Band::getDefaultAnalysisFrequencyMHz))
|
return PropagationFrequencyResolver.resolve(
|
||||||
.orElse(null);
|
resolveCallsignVariants(member),
|
||||||
}
|
getEnabledStationBands(),
|
||||||
|
System.currentTimeMillis()
|
||||||
// Known evidence exists, but every matching band is disabled or NOT QRV.
|
);
|
||||||
if (resolution.hasBandEvidence()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
EnumSet<Band> fallbackBands = EnumSet.copyOf(enabledBands);
|
|
||||||
fallbackBands.removeAll(resolution.getNotQrvBands());
|
|
||||||
if (fallbackBands.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (member != null
|
|
||||||
&& member.getChatCategory() != null
|
|
||||||
&& member.getChatCategory().getCategoryNumber() == ChatCategory.MICROWAVE
|
|
||||||
&& fallbackBands.contains(Band.B_1296)) {
|
|
||||||
return Band.B_1296;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (member != null
|
|
||||||
&& member.getChatCategory() != null
|
|
||||||
&& member.getChatCategory().getCategoryNumber() == ChatCategory.FIFTYSEVENTYMHz) {
|
|
||||||
if (fallbackBands.contains(Band.B_50)) {
|
|
||||||
return Band.B_50;
|
|
||||||
}
|
|
||||||
if (fallbackBands.contains(Band.B_70)) {
|
|
||||||
return Band.B_70;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fallbackBands.contains(Band.B_144)) {
|
|
||||||
return Band.B_144;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallbackBands.stream()
|
|
||||||
.min(Comparator.comparingDouble(Band::getDefaultAnalysisFrequencyMHz))
|
|
||||||
.orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -329,27 +291,7 @@ public final class ReachabilityService {
|
|||||||
return variants.isEmpty() ? List.of(member) : variants;
|
return variants.isEmpty() ? List.of(member) : variants;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies that an automatically selected map/snapshot frequency belongs to a
|
|
||||||
* locally enabled band that is still available after NOT-QRV resolution.
|
|
||||||
* Manual UI band overrides are handled separately and are not changed here.
|
|
||||||
*/
|
|
||||||
private boolean isUsableAutomaticBand(ChatMember member, Band band) {
|
|
||||||
if (band == null || !getEnabledStationBands().contains(band)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (member == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
BandOpportunityResolver.Resolution resolution = BandOpportunityResolver.resolve(
|
|
||||||
resolveCallsignVariants(member),
|
|
||||||
System.currentTimeMillis()
|
|
||||||
);
|
|
||||||
|
|
||||||
return resolution.getAvailableBands().contains(band);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the background executor.
|
* Stops the background executor.
|
||||||
@@ -541,28 +483,7 @@ public final class ReachabilityService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the analysis frequency from a map snapshot first, because the map
|
|
||||||
* aggregates all visible ChatMember variants and often knows the best current
|
|
||||||
* frequency per band.
|
|
||||||
*
|
|
||||||
* @param member fallback member
|
|
||||||
* @param selectedSnapshot selected map snapshot
|
|
||||||
* @return analysis frequency in MHz
|
|
||||||
*/
|
|
||||||
private double resolveAnalysisFrequencyForSnapshot(ChatMember member, MapCallsignRawSnapshot selectedSnapshot) {
|
|
||||||
if (selectedSnapshot != null) {
|
|
||||||
double snapshotFrequencyMHz =
|
|
||||||
PathGeometryUtils.resolveAnalysisFrequencyMHz(selectedSnapshot.lastKnownFrequenciesByBand());
|
|
||||||
|
|
||||||
if (Double.isFinite(snapshotFrequencyMHz) && snapshotFrequencyMHz > 0.0) {
|
|
||||||
return snapshotFrequencyMHz;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Band fallbackBand = member == null ? Band.B_144 : resolveAutoBand(member);
|
|
||||||
return resolveAnalysisFrequencyForBand(member, fallbackBand);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the analysis frequency for one member/band pair.
|
* Resolves the analysis frequency for one member/band pair.
|
||||||
@@ -575,15 +496,28 @@ public final class ReachabilityService {
|
|||||||
* </ol>
|
* </ol>
|
||||||
*/
|
*/
|
||||||
private double resolveAnalysisFrequencyForBand(ChatMember member, Band band) {
|
private double resolveAnalysisFrequencyForBand(ChatMember member, Band band) {
|
||||||
if (member != null && member.getKnownActiveBands() != null) {
|
if (band == null) {
|
||||||
ChatMember.ActiveFrequencyInfo activeFrequencyInfo = member.getKnownActiveBands().get(band);
|
return Double.NaN;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatMember.ActiveFrequencyInfo latestFrequencyInfo = null;
|
||||||
|
for (ChatMember variant : resolveCallsignVariants(member)) {
|
||||||
|
ChatMember.ActiveFrequencyInfo activeFrequencyInfo =
|
||||||
|
variant.getKnownActiveBands().get(band);
|
||||||
|
|
||||||
if (activeFrequencyInfo != null
|
if (activeFrequencyInfo != null
|
||||||
&& Double.isFinite(activeFrequencyInfo.frequency)
|
&& Double.isFinite(activeFrequencyInfo.frequency)
|
||||||
&& activeFrequencyInfo.frequency > 0.0) {
|
&& activeFrequencyInfo.frequency > 0.0
|
||||||
return activeFrequencyInfo.frequency;
|
&& (latestFrequencyInfo == null
|
||||||
|
|| activeFrequencyInfo.timestampEpoch > latestFrequencyInfo.timestampEpoch)) {
|
||||||
|
latestFrequencyInfo = activeFrequencyInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (latestFrequencyInfo != null) {
|
||||||
|
return latestFrequencyInfo.frequency;
|
||||||
|
}
|
||||||
|
|
||||||
if (member != null && member.getFrequency() != null && member.getFrequency().getValue() != null) {
|
if (member != null && member.getFrequency() != null && member.getFrequency().getValue() != null) {
|
||||||
double parsedFrequencyMHz = PathGeometryUtils.tryParseFrequencyMHz(member.getFrequency().getValue());
|
double parsedFrequencyMHz = PathGeometryUtils.tryParseFrequencyMHz(member.getFrequency().getValue());
|
||||||
if (Double.isFinite(parsedFrequencyMHz) && parsedFrequencyMHz > 0.0) {
|
if (Double.isFinite(parsedFrequencyMHz) && parsedFrequencyMHz > 0.0) {
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
package kst4contest.logic;
|
||||||
|
|
||||||
|
import kst4contest.model.Band;
|
||||||
|
import kst4contest.model.ChatCategory;
|
||||||
|
import kst4contest.model.ChatMember;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects one realistic propagation frequency for a station.
|
||||||
|
*
|
||||||
|
* <p>The same resolution is used by AirScout and by the internal path analysis.
|
||||||
|
* Only the chat categories supported by these features participate. This keeps
|
||||||
|
* unrelated KST categories from silently falling back to 144 MHz.</p>
|
||||||
|
*/
|
||||||
|
public final class PropagationFrequencyResolver {
|
||||||
|
|
||||||
|
// private static final double DUAL_VUHF_MICROWAVE_FALLBACK_MHZ = 430.0;
|
||||||
|
|
||||||
|
private PropagationFrequencyResolver() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Explains why a frequency was selected. */
|
||||||
|
public enum Source {
|
||||||
|
CURRENT_QRG,
|
||||||
|
STATION_NAME,
|
||||||
|
DUAL_CATEGORY_FALLBACK,
|
||||||
|
CHAT_CATEGORY
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the frequency from all active category variants of one base
|
||||||
|
* callsign.
|
||||||
|
*
|
||||||
|
* <ol>
|
||||||
|
* <li>Most recently detected QRG</li>
|
||||||
|
* <li>Lowest band explicitly named by the station</li>
|
||||||
|
* <li>432 MHz if the station is present in VUHF and Microwave</li>
|
||||||
|
* <li>Lowest usable fallback band of the supported chat category</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>Locally disabled and manually excluded bands are never selected.</p>
|
||||||
|
*
|
||||||
|
* @param variants active category variants of one callsign
|
||||||
|
* @param enabledBands bands enabled for the local station
|
||||||
|
* @param nowEpochMs current time used for the QRG age check
|
||||||
|
* @return one resolution, or {@code null} if no safe choice exists
|
||||||
|
*/
|
||||||
|
public static Resolution resolve(Collection<ChatMember> variants,
|
||||||
|
EnumSet<Band> enabledBands,
|
||||||
|
long nowEpochMs) {
|
||||||
|
|
||||||
|
if (variants == null || variants.isEmpty()
|
||||||
|
|| enabledBands == null || enabledBands.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChatMember> supportedVariants = variants.stream()
|
||||||
|
.filter(PropagationFrequencyResolver::isSupportedVariant)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (supportedVariants.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
BandOpportunityResolver.Resolution opportunityResolution =
|
||||||
|
BandOpportunityResolver.resolve(supportedVariants, nowEpochMs);
|
||||||
|
|
||||||
|
EnumSet<Band> usableBands = EnumSet.copyOf(enabledBands);
|
||||||
|
usableBands.removeAll(opportunityResolution.getNotQrvBands());
|
||||||
|
|
||||||
|
if (usableBands.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
FrequencyCandidate latestQrg = findLatestQrg(
|
||||||
|
supportedVariants,
|
||||||
|
usableBands,
|
||||||
|
nowEpochMs
|
||||||
|
);
|
||||||
|
|
||||||
|
if (latestQrg != null) {
|
||||||
|
return new Resolution(
|
||||||
|
latestQrg.band,
|
||||||
|
latestQrg.frequencyMHz,
|
||||||
|
Source.CURRENT_QRG
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EnumSet<Band> nameBands = EnumSet.noneOf(Band.class);
|
||||||
|
for (ChatMember variant : supportedVariants) {
|
||||||
|
nameBands.addAll(
|
||||||
|
BandOpportunityResolver.detectBandsFromStationName(variant.getName())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
nameBands.retainAll(usableBands);
|
||||||
|
|
||||||
|
Band nameBand = lowestBand(nameBands);
|
||||||
|
if (nameBand != null) {
|
||||||
|
return new Resolution(
|
||||||
|
nameBand,
|
||||||
|
nameBand.getDefaultAnalysisFrequencyMHz(),
|
||||||
|
Source.STATION_NAME
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EnumSet<SupportedCategory> categories = collectSupportedCategories(supportedVariants);
|
||||||
|
|
||||||
|
if (categories.contains(SupportedCategory.VUHF)
|
||||||
|
&& categories.contains(SupportedCategory.MICROWAVE)
|
||||||
|
&& usableBands.contains(Band.B_432)) {
|
||||||
|
return new Resolution(
|
||||||
|
Band.B_432,
|
||||||
|
Band.B_432.getDefaultAnalysisFrequencyMHz(),
|
||||||
|
Source.DUAL_CATEGORY_FALLBACK
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EnumSet<Band> categoryBands = EnumSet.noneOf(Band.class);
|
||||||
|
for (SupportedCategory category : categories) {
|
||||||
|
categoryBands.addAll(category.fallbackBands);
|
||||||
|
}
|
||||||
|
categoryBands.retainAll(usableBands);
|
||||||
|
|
||||||
|
Band categoryBand = lowestBand(categoryBands);
|
||||||
|
if (categoryBand == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Resolution(
|
||||||
|
categoryBand,
|
||||||
|
categoryBand.getDefaultAnalysisFrequencyMHz(),
|
||||||
|
Source.CHAT_CATEGORY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FrequencyCandidate findLatestQrg(List<ChatMember> variants,
|
||||||
|
EnumSet<Band> usableBands,
|
||||||
|
long nowEpochMs) {
|
||||||
|
FrequencyCandidate latest = null;
|
||||||
|
|
||||||
|
for (ChatMember variant : variants) {
|
||||||
|
for (var entry : variant.getKnownActiveBands().entrySet()) {
|
||||||
|
Band band = entry.getKey();
|
||||||
|
ChatMember.ActiveFrequencyInfo info = entry.getValue();
|
||||||
|
|
||||||
|
if (band == null || info == null || !usableBands.contains(band)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long ageMs = nowEpochMs - info.timestampEpoch;
|
||||||
|
if (ageMs < 0L
|
||||||
|
|| ageMs > BandOpportunityResolver.RECENT_DYNAMIC_EVIDENCE_MAX_AGE_MS
|
||||||
|
|| !Double.isFinite(info.frequency)
|
||||||
|
|| !band.isPlausible(info.frequency)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latest == null || info.timestampEpoch > latest.timestampEpochMs) {
|
||||||
|
latest = new FrequencyCandidate(
|
||||||
|
band,
|
||||||
|
info.frequency,
|
||||||
|
info.timestampEpoch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSupportedVariant(ChatMember member) {
|
||||||
|
if (member == null || member.getChatCategory() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int categoryNumber = member.getChatCategory().getCategoryNumber();
|
||||||
|
return categoryNumber == ChatCategory.FIFTYSEVENTYMHz
|
||||||
|
|| categoryNumber == ChatCategory.VUHF
|
||||||
|
|| categoryNumber == ChatCategory.MICROWAVE
|
||||||
|
|| categoryNumber == ChatCategory.EMEJT65;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EnumSet<SupportedCategory> collectSupportedCategories(
|
||||||
|
List<ChatMember> variants
|
||||||
|
) {
|
||||||
|
EnumSet<SupportedCategory> categories = EnumSet.noneOf(SupportedCategory.class);
|
||||||
|
|
||||||
|
for (ChatMember variant : variants) {
|
||||||
|
int categoryNumber = variant.getChatCategory().getCategoryNumber();
|
||||||
|
|
||||||
|
if (categoryNumber == ChatCategory.FIFTYSEVENTYMHz) {
|
||||||
|
categories.add(SupportedCategory.FIFTY_SEVENTY);
|
||||||
|
} else if (categoryNumber == ChatCategory.VUHF) {
|
||||||
|
categories.add(SupportedCategory.VUHF);
|
||||||
|
} else if (categoryNumber == ChatCategory.MICROWAVE) {
|
||||||
|
categories.add(SupportedCategory.MICROWAVE);
|
||||||
|
} else if (categoryNumber == ChatCategory.EMEJT65) {
|
||||||
|
categories.add(SupportedCategory.EME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Band lowestBand(Collection<Band> bands) {
|
||||||
|
if (bands == null || bands.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bands.stream()
|
||||||
|
.min(Comparator.comparingDouble(Band::getDefaultAnalysisFrequencyMHz))
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SupportedCategory {
|
||||||
|
FIFTY_SEVENTY(EnumSet.of(Band.B_50, Band.B_70)),
|
||||||
|
VUHF(EnumSet.of(Band.B_144, Band.B_432)),
|
||||||
|
MICROWAVE(EnumSet.of(
|
||||||
|
Band.B_1296,
|
||||||
|
Band.B_2320,
|
||||||
|
Band.B_3400,
|
||||||
|
Band.B_5760,
|
||||||
|
Band.B_10G,
|
||||||
|
Band.B_24G
|
||||||
|
)),
|
||||||
|
EME(EnumSet.of(
|
||||||
|
Band.B_144,
|
||||||
|
Band.B_432,
|
||||||
|
Band.B_1296,
|
||||||
|
Band.B_2320,
|
||||||
|
Band.B_3400,
|
||||||
|
Band.B_5760,
|
||||||
|
Band.B_10G,
|
||||||
|
Band.B_24G
|
||||||
|
));
|
||||||
|
|
||||||
|
private final EnumSet<Band> fallbackBands;
|
||||||
|
|
||||||
|
SupportedCategory(EnumSet<Band> fallbackBands) {
|
||||||
|
this.fallbackBands = fallbackBands;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class FrequencyCandidate {
|
||||||
|
|
||||||
|
private final Band band;
|
||||||
|
private final double frequencyMHz;
|
||||||
|
private final long timestampEpochMs;
|
||||||
|
|
||||||
|
private FrequencyCandidate(Band band,
|
||||||
|
double frequencyMHz,
|
||||||
|
long timestampEpochMs) {
|
||||||
|
this.band = band;
|
||||||
|
this.frequencyMHz = frequencyMHz;
|
||||||
|
this.timestampEpochMs = timestampEpochMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Immutable selected band/frequency pair. */
|
||||||
|
public static final class Resolution {
|
||||||
|
|
||||||
|
private final Band band;
|
||||||
|
private final double analysisFrequencyMHz;
|
||||||
|
private final Source source;
|
||||||
|
|
||||||
|
private Resolution(Band band,
|
||||||
|
double analysisFrequencyMHz,
|
||||||
|
Source source) {
|
||||||
|
this.band = band;
|
||||||
|
this.analysisFrequencyMHz = analysisFrequencyMHz;
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Band getBand() {
|
||||||
|
return band;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getAnalysisFrequencyMHz() {
|
||||||
|
return analysisFrequencyMHz;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Source getSource() {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts MHz to AirScout's 100-Hz protocol unit.
|
||||||
|
*
|
||||||
|
* @return integer protocol value, for example 1442100 for 144.210 MHz
|
||||||
|
*/
|
||||||
|
public String getAirScoutBandValue() {
|
||||||
|
return Long.toString(Math.round(analysisFrequencyMHz * 10_000.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -245,6 +245,7 @@ public class ChatPreferences {
|
|||||||
boolean AirScout_asUDPListenerEnabled = true;
|
boolean AirScout_asUDPListenerEnabled = true;
|
||||||
String AirScout_asServerNameString = "AS";
|
String AirScout_asServerNameString = "AS";
|
||||||
String AirScout_asClientNameString = "KST";
|
String AirScout_asClientNameString = "KST";
|
||||||
|
boolean AirScout_autoBandSelectionEnabled = true;
|
||||||
String AirScout_asBandString = "1440000";
|
String AirScout_asBandString = "1440000";
|
||||||
int AirScout_asCommunicationPort = 9872;
|
int AirScout_asCommunicationPort = 9872;
|
||||||
|
|
||||||
@@ -1004,6 +1005,16 @@ public class ChatPreferences {
|
|||||||
return AirScout_asBandString;
|
return AirScout_asBandString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isAirScout_autoBandSelectionEnabled() {
|
||||||
|
return AirScout_autoBandSelectionEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAirScout_autoBandSelectionEnabled(
|
||||||
|
boolean airScoutAutoBandSelectionEnabled
|
||||||
|
) {
|
||||||
|
AirScout_autoBandSelectionEnabled = airScoutAutoBandSelectionEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
public void setAirScout_asBandString(String airScout_asBandString) {
|
public void setAirScout_asBandString(String airScout_asBandString) {
|
||||||
if (airScout_asBandString == null) {
|
if (airScout_asBandString == null) {
|
||||||
AirScout_asBandString = "1440000";
|
AirScout_asBandString = "1440000";
|
||||||
@@ -1695,6 +1706,13 @@ public class ChatPreferences {
|
|||||||
asQry_airScoutUDPPort.setTextContent(this.getAirScout_asCommunicationPort()+"");
|
asQry_airScoutUDPPort.setTextContent(this.getAirScout_asCommunicationPort()+"");
|
||||||
AirScoutQuerier.appendChild(asQry_airScoutUDPPort);
|
AirScoutQuerier.appendChild(asQry_airScoutUDPPort);
|
||||||
|
|
||||||
|
Element asQry_airScoutAutoBandSelectionEnabled =
|
||||||
|
doc.createElement("asQry_airScoutAutoBandSelectionEnabled");
|
||||||
|
asQry_airScoutAutoBandSelectionEnabled.setTextContent(
|
||||||
|
Boolean.toString(this.isAirScout_autoBandSelectionEnabled())
|
||||||
|
);
|
||||||
|
AirScoutQuerier.appendChild(asQry_airScoutAutoBandSelectionEnabled);
|
||||||
|
|
||||||
Element asQry_airScoutBandValue = doc.createElement("asQry_airScoutBandValue");
|
Element asQry_airScoutBandValue = doc.createElement("asQry_airScoutBandValue");
|
||||||
asQry_airScoutBandValue.setTextContent(this.getAirScout_asBandString());
|
asQry_airScoutBandValue.setTextContent(this.getAirScout_asBandString());
|
||||||
AirScoutQuerier.appendChild(asQry_airScoutBandValue);
|
AirScoutQuerier.appendChild(asQry_airScoutBandValue);
|
||||||
@@ -2508,6 +2526,14 @@ public class ChatPreferences {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setAirScout_autoBandSelectionEnabled(
|
||||||
|
getBoolean(
|
||||||
|
airScoutEl,
|
||||||
|
AirScout_autoBandSelectionEnabled,
|
||||||
|
"asQry_airScoutAutoBandSelectionEnabled"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
setAirScout_asBandString(
|
setAirScout_asBandString(
|
||||||
getText(
|
getText(
|
||||||
airScoutEl,
|
airScoutEl,
|
||||||
@@ -2525,6 +2551,8 @@ public class ChatPreferences {
|
|||||||
+ AirScout_asClientNameString
|
+ AirScout_asClientNameString
|
||||||
+ ", port="
|
+ ", port="
|
||||||
+ AirScout_asCommunicationPort
|
+ AirScout_asCommunicationPort
|
||||||
|
+ ", automatic band selection="
|
||||||
|
+ AirScout_autoBandSelectionEnabled
|
||||||
+ ", band="
|
+ ", band="
|
||||||
+ AirScout_asBandString
|
+ AirScout_asBandString
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -178,7 +178,8 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
chatcontroller,
|
chatcontroller,
|
||||||
tbl_chatMember,
|
tbl_chatMember,
|
||||||
stationMapView,
|
stationMapView,
|
||||||
this::focusChatMemberAndPrepareCq
|
this::focusChatMemberAndPrepareCq,
|
||||||
|
() -> selectedReachabilityBandOverride
|
||||||
);
|
);
|
||||||
stationMapBridge.install();
|
stationMapBridge.install();
|
||||||
}
|
}
|
||||||
@@ -7958,6 +7959,15 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
|
|
||||||
Band selectedBand = resolveReachabilityBandForUi(selectedMember);
|
Band selectedBand = resolveReachabilityBandForUi(selectedMember);
|
||||||
chatcontroller.getReachabilityService().calculateSelectedStationOnDemand(selectedMember, selectedBand);
|
chatcontroller.getReachabilityService().calculateSelectedStationOnDemand(selectedMember, selectedBand);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If the map is already initialized, make it request the same operator-selected
|
||||||
|
* band. ReachabilityService deduplicates the identical calculation key, so this
|
||||||
|
* attaches the map callback without causing a second terrain API request.
|
||||||
|
*/
|
||||||
|
if (stationMapBridge != null) {
|
||||||
|
stationMapBridge.requestSelectedPathAnalysisRefresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -7970,7 +7980,10 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
cmbReachabilityBand.getItems().add(band.getDisplayLabel());
|
cmbReachabilityBand.getItems().add(band.getDisplayLabel());
|
||||||
}
|
}
|
||||||
cmbReachabilityBand.getSelectionModel().select("Auto");
|
cmbReachabilityBand.getSelectionModel().select("Auto");
|
||||||
cmbReachabilityBand.setTooltip(new Tooltip("Reachability band for Tropo column/filter. Auto uses the station's lowest session band."));
|
cmbReachabilityBand.setTooltip(new Tooltip(
|
||||||
|
"Reachability band for Tropo column/filter. Auto uses the current "
|
||||||
|
+ "QRG, station-name hints and the supported chat category."
|
||||||
|
));
|
||||||
cmbReachabilityBand.setOnAction(new EventHandler<ActionEvent>() {
|
cmbReachabilityBand.setOnAction(new EventHandler<ActionEvent>() {
|
||||||
@Override
|
@Override
|
||||||
public void handle(ActionEvent event) {
|
public void handle(ActionEvent event) {
|
||||||
@@ -9038,6 +9051,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
|
|
||||||
TextField txtFldstn_pathAnalysisDemRootDirectory =
|
TextField txtFldstn_pathAnalysisDemRootDirectory =
|
||||||
new TextField(this.chatcontroller.getChatPreferences().getStn_pathAnalysisDemRootDirectory());
|
new TextField(this.chatcontroller.getChatPreferences().getStn_pathAnalysisDemRootDirectory());
|
||||||
|
txtFldstn_pathAnalysisDemRootDirectory.setDisable(true);
|
||||||
txtFldstn_pathAnalysisDemRootDirectory.setFocusTraversable(false);
|
txtFldstn_pathAnalysisDemRootDirectory.setFocusTraversable(false);
|
||||||
txtFldstn_pathAnalysisDemRootDirectory.setTooltip(new Tooltip(
|
txtFldstn_pathAnalysisDemRootDirectory.setTooltip(new Tooltip(
|
||||||
"Root directory that contains locally extracted Copernicus GLO-30 DEM tiles.\n" +
|
"Root directory that contains locally extracted Copernicus GLO-30 DEM tiles.\n" +
|
||||||
@@ -9058,6 +9072,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
OfflineDemImportService offlineDemImportService = new OfflineDemImportService();
|
OfflineDemImportService offlineDemImportService = new OfflineDemImportService();
|
||||||
|
|
||||||
Button btnUseDefaultDemDirectory = new Button("Default");
|
Button btnUseDefaultDemDirectory = new Button("Default");
|
||||||
|
btnUseDefaultDemDirectory.setDisable(true);
|
||||||
btnUseDefaultDemDirectory.setFocusTraversable(false);
|
btnUseDefaultDemDirectory.setFocusTraversable(false);
|
||||||
btnUseDefaultDemDirectory.setTooltip(new Tooltip(
|
btnUseDefaultDemDirectory.setTooltip(new Tooltip(
|
||||||
"Creates and uses the default local Copernicus DEM directory below .praktiKST.\n" +
|
"Creates and uses the default local Copernicus DEM directory below .praktiKST.\n" +
|
||||||
@@ -9087,6 +9102,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
});
|
});
|
||||||
|
|
||||||
Button btnImportDemTiles = new Button("Import tiles...");
|
Button btnImportDemTiles = new Button("Import tiles...");
|
||||||
|
btnImportDemTiles.setDisable(true);
|
||||||
btnImportDemTiles.setFocusTraversable(false);
|
btnImportDemTiles.setFocusTraversable(false);
|
||||||
btnImportDemTiles.setTooltip(new Tooltip(
|
btnImportDemTiles.setTooltip(new Tooltip(
|
||||||
"Copies manually selected Copernicus *_DEM.tif files into the configured DEM root directory.\n" +
|
"Copies manually selected Copernicus *_DEM.tif files into the configured DEM root directory.\n" +
|
||||||
@@ -9938,8 +9954,11 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
Label lblASUdpPort =
|
Label lblASUdpPort =
|
||||||
new Label("AirScout UDP port [9872] — reconnect after changing:");
|
new Label("AirScout UDP port [9872] — reconnect after changing:");
|
||||||
|
|
||||||
|
Label lblASAutoBand =
|
||||||
|
new Label("Select AirScout frequency automatically per station:");
|
||||||
|
|
||||||
Label lblASBandName =
|
Label lblASBandName =
|
||||||
new Label("AirScout band value [1440000 = 144 MHz]:");
|
new Label("Forced AirScout band value [1440000 = 144 MHz]:");
|
||||||
|
|
||||||
CheckBox chkBxEnableUDPMsgbyAS = new CheckBox();
|
CheckBox chkBxEnableUDPMsgbyAS = new CheckBox();
|
||||||
chkBxEnableUDPMsgbyAS.setSelected(
|
chkBxEnableUDPMsgbyAS.setSelected(
|
||||||
@@ -10092,13 +10111,38 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
chatcontroller.getChatPreferences()
|
chatcontroller.getChatPreferences()
|
||||||
.getAirScout_asBandString()
|
.getAirScout_asBandString()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CheckBox chkBxAutoAirScoutBand = new CheckBox("Auto per station");
|
||||||
|
chkBxAutoAirScoutBand.setSelected(
|
||||||
|
chatcontroller.getChatPreferences()
|
||||||
|
.isAirScout_autoBandSelectionEnabled()
|
||||||
|
);
|
||||||
|
chkBxAutoAirScoutBand.setTooltip(
|
||||||
|
new Tooltip(
|
||||||
|
"Uses the station's current QRG first, then station-name and "
|
||||||
|
+ "chat-category evidence. Disable this option only to force "
|
||||||
|
+ "one protocol value for every station."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
txtFld_asQRGInt.setDisable(chkBxAutoAirScoutBand.isSelected());
|
||||||
|
chkBxAutoAirScoutBand.selectedProperty().addListener(
|
||||||
|
(observable, oldValue, newValue) -> {
|
||||||
|
chatcontroller.getChatPreferences()
|
||||||
|
.setAirScout_autoBandSelectionEnabled(newValue);
|
||||||
|
txtFld_asQRGInt.setDisable(newValue);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
txtFld_asQRGInt.setFocusTraversable(false);
|
txtFld_asQRGInt.setFocusTraversable(false);
|
||||||
txtFld_asQRGInt.setTooltip(
|
txtFld_asQRGInt.setTooltip(
|
||||||
new Tooltip(
|
new Tooltip(
|
||||||
"AirScout protocol band value, for example 1440000 for "
|
"Fallback used only when automatic per-station selection is "
|
||||||
+ "144 MHz or 4320000 for 432 MHz."
|
+ "disabled. Examples: 1440000 for 144 MHz or 4320000 "
|
||||||
|
+ "for 432 MHz."
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
txtFld_asQRGInt.focusedProperty().addListener(
|
txtFld_asQRGInt.focusedProperty().addListener(
|
||||||
(observable, oldValue, newValue) -> {
|
(observable, oldValue, newValue) -> {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
@@ -10133,7 +10177,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
);
|
);
|
||||||
|
|
||||||
Label lblASChangeNote = new Label(
|
Label lblASChangeNote = new Label(
|
||||||
"Server identifier, client identifier and band are applied "
|
"Server identifier, client identifier and frequency mode are applied "
|
||||||
+ "immediately. Reconnect after changing the UDP port."
|
+ "immediately. Reconnect after changing the UDP port."
|
||||||
);
|
);
|
||||||
lblASChangeNote.setWrapText(true);
|
lblASChangeNote.setWrapText(true);
|
||||||
@@ -10156,9 +10200,11 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
grdPnlAirScout.add(txtFld_asClientNameString, 1, 3);
|
grdPnlAirScout.add(txtFld_asClientNameString, 1, 3);
|
||||||
grdPnlAirScout.add(lblASUdpPort, 0, 4);
|
grdPnlAirScout.add(lblASUdpPort, 0, 4);
|
||||||
grdPnlAirScout.add(txtFld_asUDPPortInt, 1, 4);
|
grdPnlAirScout.add(txtFld_asUDPPortInt, 1, 4);
|
||||||
grdPnlAirScout.add(lblASBandName, 0, 5);
|
grdPnlAirScout.add(lblASAutoBand, 0, 5);
|
||||||
grdPnlAirScout.add(txtFld_asQRGInt, 1, 5);
|
grdPnlAirScout.add(chkBxAutoAirScoutBand, 1, 5);
|
||||||
grdPnlAirScout.add(lblASChangeNote, 0, 6, 2, 1);
|
grdPnlAirScout.add(lblASBandName, 0, 6);
|
||||||
|
grdPnlAirScout.add(txtFld_asQRGInt, 1, 6);
|
||||||
|
grdPnlAirScout.add(lblASChangeNote, 0, 7, 2, 1);
|
||||||
|
|
||||||
VBox vbxAirScout = new VBox();
|
VBox vbxAirScout = new VBox();
|
||||||
vbxAirScout.setPadding(new Insets(10, 10, 10, 10));
|
vbxAirScout.setPadding(new Insets(10, 10, 10, 10));
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package kst4contest.view.map;
|
package kst4contest.view.map;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -496,37 +495,7 @@ public final class PathGeometryUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves one usable analysis frequency from aggregated marker frequency data.
|
|
||||||
*
|
|
||||||
* <p>Strategy:
|
|
||||||
* <ol>
|
|
||||||
* <li>Prefer 144 MHz data if available</li>
|
|
||||||
* <li>Otherwise use the first parsable known station frequency</li>
|
|
||||||
* <li>Otherwise use the central default frequency</li>
|
|
||||||
* </ol>
|
|
||||||
*
|
|
||||||
* @param frequenciesByBand known frequencies grouped by band
|
|
||||||
* @return resolved analysis frequency in MHz
|
|
||||||
*/
|
|
||||||
public static double resolveAnalysisFrequencyMHz(Map<String, String> frequenciesByBand) {
|
|
||||||
if (frequenciesByBand != null && !frequenciesByBand.isEmpty()) {
|
|
||||||
String band144Text = frequenciesByBand.get("144");
|
|
||||||
double parsed144 = tryParseFrequencyMHz(band144Text);
|
|
||||||
if (Double.isFinite(parsed144) && parsed144 > 0.0) {
|
|
||||||
return parsed144;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String value : frequenciesByBand.values()) {
|
|
||||||
double parsedFrequencyMHz = tryParseFrequencyMHz(value);
|
|
||||||
if (Double.isFinite(parsedFrequencyMHz) && parsedFrequencyMHz > 0.0) {
|
|
||||||
return parsedFrequencyMHz;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return DEFAULT_ANALYSIS_FREQUENCY_MHZ;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Small immutable geographic point used by great-circle interpolation.
|
* Small immutable geographic point used by great-circle interpolation.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import java.util.Objects;
|
|||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import kst4contest.model.Band;
|
import kst4contest.model.Band;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
@@ -40,6 +41,7 @@ public final class StationMapBridge {
|
|||||||
private final TableView<ChatMember> chatMemberTable;
|
private final TableView<ChatMember> chatMemberTable;
|
||||||
private final StationMapView stationMapView;
|
private final StationMapView stationMapView;
|
||||||
private final Consumer<ChatMember> focusChatMemberConsumer;
|
private final Consumer<ChatMember> focusChatMemberConsumer;
|
||||||
|
private final Supplier<Band> reachabilityBandOverrideSupplier;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -55,16 +57,22 @@ public final class StationMapBridge {
|
|||||||
public StationMapBridge(ChatController chatController,
|
public StationMapBridge(ChatController chatController,
|
||||||
TableView<ChatMember> chatMemberTable,
|
TableView<ChatMember> chatMemberTable,
|
||||||
StationMapView stationMapView,
|
StationMapView stationMapView,
|
||||||
Consumer<ChatMember> focusChatMemberConsumer) {
|
Consumer<ChatMember> focusChatMemberConsumer,
|
||||||
|
Supplier<Band> reachabilityBandOverrideSupplier) {
|
||||||
|
|
||||||
this.chatController = Objects.requireNonNull(chatController, "chatController");
|
this.chatController = Objects.requireNonNull(chatController, "chatController");
|
||||||
this.chatMemberTable = Objects.requireNonNull(chatMemberTable, "chatMemberTable");
|
this.chatMemberTable = Objects.requireNonNull(chatMemberTable, "chatMemberTable");
|
||||||
this.stationMapView = Objects.requireNonNull(stationMapView, "stationMapView");
|
this.stationMapView = Objects.requireNonNull(stationMapView, "stationMapView");
|
||||||
this.focusChatMemberConsumer = Objects.requireNonNull(focusChatMemberConsumer, "focusChatMemberConsumer");
|
this.focusChatMemberConsumer = Objects.requireNonNull(
|
||||||
|
focusChatMemberConsumer,
|
||||||
|
"focusChatMemberConsumer"
|
||||||
|
);
|
||||||
|
this.reachabilityBandOverrideSupplier = Objects.requireNonNull(
|
||||||
|
reachabilityBandOverrideSupplier,
|
||||||
|
"reachabilityBandOverrideSupplier"
|
||||||
|
);
|
||||||
|
|
||||||
this.refreshCoalescer.setOnFinished(event -> refreshNow());
|
this.refreshCoalescer.setOnFinished(event -> refreshNow());
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void install() {
|
public void install() {
|
||||||
@@ -115,6 +123,18 @@ public final class StationMapBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces the currently selected map path to be requested again.
|
||||||
|
*
|
||||||
|
* <p>This is used by the explicit "Calc selected" action after the operator
|
||||||
|
* changed the reachability band. Merely changing the ComboBox still does not
|
||||||
|
* trigger terrain analysis.</p>
|
||||||
|
*/
|
||||||
|
public void requestSelectedPathAnalysisRefresh() {
|
||||||
|
lastPathAnalysisRequestSignature = "";
|
||||||
|
requestImmediateRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
public void focusSelectedCallsign() {
|
public void focusSelectedCallsign() {
|
||||||
showWindow();
|
showWindow();
|
||||||
|
|
||||||
@@ -222,9 +242,12 @@ public final class StationMapBridge {
|
|||||||
|
|
||||||
ChatMember selectedMember = resolveBestChatMember(targetCallsignRaw);
|
ChatMember selectedMember = resolveBestChatMember(targetCallsignRaw);
|
||||||
|
|
||||||
|
Band requestedBandOverride = reachabilityBandOverrideSupplier.get();
|
||||||
|
|
||||||
chatController.getReachabilityService().requestPathAnalysisForMap(
|
chatController.getReachabilityService().requestPathAnalysisForMap(
|
||||||
selectedMember,
|
selectedMember,
|
||||||
selectedSnapshot,
|
selectedSnapshot,
|
||||||
|
requestedBandOverride,
|
||||||
result -> {
|
result -> {
|
||||||
if (generation != pathAnalysisGeneration.get()) {
|
if (generation != pathAnalysisGeneration.get()) {
|
||||||
return;
|
return;
|
||||||
@@ -232,6 +255,7 @@ public final class StationMapBridge {
|
|||||||
stationMapView.setPathAnalysisResult(result);
|
stationMapView.setPathAnalysisResult(result);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -259,6 +283,13 @@ public final class StationMapBridge {
|
|||||||
chatMemberTable.scrollTo(resolved);
|
chatMemberTable.scrollTo(resolved);
|
||||||
|
|
||||||
focusChatMemberConsumer.accept(resolved);
|
focusChatMemberConsumer.accept(resolved);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A map click is an explicit operator action. Clear the signature so a
|
||||||
|
* newly selected reachability band is honored even when the same station
|
||||||
|
* is clicked again.
|
||||||
|
*/
|
||||||
|
lastPathAnalysisRequestSignature = "";
|
||||||
requestImmediateRefresh();
|
requestImmediateRefresh();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -315,10 +346,16 @@ public final class StationMapBridge {
|
|||||||
|
|
||||||
private double resolveAnalysisFrequencyMHz(MapCallsignRawSnapshot selectedSnapshot) {
|
private double resolveAnalysisFrequencyMHz(MapCallsignRawSnapshot selectedSnapshot) {
|
||||||
if (selectedSnapshot == null) {
|
if (selectedSnapshot == null) {
|
||||||
return PathGeometryUtils.DEFAULT_ANALYSIS_FREQUENCY_MHZ;
|
return Double.NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
return PathGeometryUtils.resolveAnalysisFrequencyMHz(selectedSnapshot.lastKnownFrequenciesByBand());
|
ChatMember selectedMember = resolveBestChatMember(selectedSnapshot.callSignRaw());
|
||||||
|
var resolution = chatController.getReachabilityService()
|
||||||
|
.resolveAutomaticPropagationFrequency(selectedMember);
|
||||||
|
|
||||||
|
return resolution == null
|
||||||
|
? Double.NaN
|
||||||
|
: resolution.getAnalysisFrequencyMHz();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package kst4contest.test;
|
||||||
|
|
||||||
|
import kst4contest.logic.PropagationFrequencyResolver;
|
||||||
|
import kst4contest.model.Band;
|
||||||
|
import kst4contest.model.ChatCategory;
|
||||||
|
import kst4contest.model.ChatMember;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
class PropagationFrequencyResolverTest {
|
||||||
|
|
||||||
|
private static final long NOW = 10_000_000L;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void currentQrgWinsOverNameAndCategoryFallback() {
|
||||||
|
ChatMember station = station(ChatCategory.MICROWAVE, "QRV 3cm");
|
||||||
|
addCurrentQrg(station, Band.B_2320, 2320.175, NOW - 1_000L);
|
||||||
|
|
||||||
|
PropagationFrequencyResolver.Resolution resolution = resolve(
|
||||||
|
List.of(station),
|
||||||
|
EnumSet.of(Band.B_1296, Band.B_2320, Band.B_10G)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(Band.B_2320, resolution.getBand());
|
||||||
|
assertEquals(2320.175, resolution.getAnalysisFrequencyMHz(), 0.000_001);
|
||||||
|
assertEquals(PropagationFrequencyResolver.Source.CURRENT_QRG, resolution.getSource());
|
||||||
|
assertEquals("23201750", resolution.getAirScoutBandValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mostRecentlyDetectedQrgWinsAcrossCategoryVariants() {
|
||||||
|
ChatMember vhf = station(ChatCategory.VUHF, "");
|
||||||
|
ChatMember microwave = station(ChatCategory.MICROWAVE, "");
|
||||||
|
addCurrentQrg(vhf, Band.B_144, 144.210, NOW - 10_000L);
|
||||||
|
addCurrentQrg(microwave, Band.B_1296, 1296.210, NOW - 1_000L);
|
||||||
|
|
||||||
|
PropagationFrequencyResolver.Resolution resolution = resolve(
|
||||||
|
List.of(vhf, microwave),
|
||||||
|
EnumSet.of(Band.B_144, Band.B_432, Band.B_1296)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(Band.B_1296, resolution.getBand());
|
||||||
|
assertEquals(1296.210, resolution.getAnalysisFrequencyMHz(), 0.000_001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void microwaveFallsBackToLowestEnabledMicrowaveBand() {
|
||||||
|
ChatMember station = station(ChatCategory.MICROWAVE, "");
|
||||||
|
|
||||||
|
PropagationFrequencyResolver.Resolution resolution = resolve(
|
||||||
|
List.of(station),
|
||||||
|
EnumSet.of(Band.B_2320, Band.B_3400)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(Band.B_2320, resolution.getBand());
|
||||||
|
assertEquals(2320.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
|
||||||
|
assertEquals(PropagationFrequencyResolver.Source.CHAT_CATEGORY, resolution.getSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void supportedCategoriesUseTheirAgreedLowestFallbackBand() {
|
||||||
|
assertEquals(
|
||||||
|
Band.B_50,
|
||||||
|
resolve(
|
||||||
|
List.of(station(ChatCategory.FIFTYSEVENTYMHz, "")),
|
||||||
|
EnumSet.of(Band.B_50, Band.B_70)
|
||||||
|
).getBand()
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
Band.B_144,
|
||||||
|
resolve(
|
||||||
|
List.of(station(ChatCategory.VUHF, "")),
|
||||||
|
EnumSet.of(Band.B_144, Band.B_432)
|
||||||
|
).getBand()
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
Band.B_1296,
|
||||||
|
resolve(
|
||||||
|
List.of(station(ChatCategory.MICROWAVE, "")),
|
||||||
|
EnumSet.of(Band.B_1296, Band.B_2320)
|
||||||
|
).getBand()
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
Band.B_144,
|
||||||
|
resolve(
|
||||||
|
List.of(station(ChatCategory.EMEJT65, "")),
|
||||||
|
EnumSet.of(Band.B_144, Band.B_1296)
|
||||||
|
).getBand()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void vhfAndMicrowaveUse432MhzFallback() {
|
||||||
|
ChatMember vhf = station(ChatCategory.VUHF, "");
|
||||||
|
ChatMember microwave = station(ChatCategory.MICROWAVE, "");
|
||||||
|
|
||||||
|
PropagationFrequencyResolver.Resolution resolution = resolve(
|
||||||
|
List.of(vhf, microwave),
|
||||||
|
EnumSet.of(Band.B_144, Band.B_432, Band.B_1296)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(Band.B_432, resolution.getBand());
|
||||||
|
assertEquals(432.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
|
||||||
|
assertEquals(
|
||||||
|
PropagationFrequencyResolver.Source.DUAL_CATEGORY_FALLBACK,
|
||||||
|
resolution.getSource()
|
||||||
|
);
|
||||||
|
assertEquals("4320000", resolution.getAirScoutBandValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unsupportedChatCategoriesDoNotFallBackTo144Mhz() {
|
||||||
|
for (int categoryNumber = ChatCategory.LOWBAND;
|
||||||
|
categoryNumber <= ChatCategory.TENMeter;
|
||||||
|
categoryNumber++) {
|
||||||
|
ChatMember unsupported = station(categoryNumber, "QRV 2m");
|
||||||
|
addCurrentQrg(unsupported, Band.B_144, 144.300, NOW - 1_000L);
|
||||||
|
|
||||||
|
assertNull(
|
||||||
|
resolve(
|
||||||
|
List.of(unsupported),
|
||||||
|
EnumSet.of(Band.B_144, Band.B_432)
|
||||||
|
),
|
||||||
|
"Category " + categoryNumber + " must be ignored"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void manualNotQrvExclusionForcesNextUsableMicrowaveBand() {
|
||||||
|
ChatMember station = station(ChatCategory.MICROWAVE, "");
|
||||||
|
station.setQrv1240(false);
|
||||||
|
|
||||||
|
PropagationFrequencyResolver.Resolution resolution = resolve(
|
||||||
|
List.of(station),
|
||||||
|
EnumSet.of(Band.B_1296, Band.B_2320)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(Band.B_2320, resolution.getBand());
|
||||||
|
assertEquals(2320.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stationNameBandHintWinsOverCategoryFallback() {
|
||||||
|
ChatMember station = station(ChatCategory.MICROWAVE, "QRV 3cm");
|
||||||
|
|
||||||
|
PropagationFrequencyResolver.Resolution resolution = resolve(
|
||||||
|
List.of(station),
|
||||||
|
EnumSet.of(Band.B_1296, Band.B_10G)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(Band.B_10G, resolution.getBand());
|
||||||
|
assertEquals(10368.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
|
||||||
|
assertEquals(PropagationFrequencyResolver.Source.STATION_NAME, resolution.getSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
private PropagationFrequencyResolver.Resolution resolve(
|
||||||
|
List<ChatMember> variants,
|
||||||
|
EnumSet<Band> enabledBands
|
||||||
|
) {
|
||||||
|
return PropagationFrequencyResolver.resolve(variants, enabledBands, NOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatMember station(int categoryNumber, String name) {
|
||||||
|
ChatMember station = new ChatMember();
|
||||||
|
station.setCallSign("DL1ABC");
|
||||||
|
station.setChatCategory(new ChatCategory(categoryNumber));
|
||||||
|
station.setName(name);
|
||||||
|
return station;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCurrentQrg(ChatMember station,
|
||||||
|
Band band,
|
||||||
|
double frequencyMHz,
|
||||||
|
long timestampEpochMs) {
|
||||||
|
station.addKnownFrequency(band, frequencyMHz);
|
||||||
|
station.getKnownActiveBands().get(band).timestampEpoch = timestampEpochMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
Reference in New Issue
Block a user