diff --git a/src/core/BandUtils.cpp b/src/core/BandUtils.cpp index e46a951..b878406 100644 --- a/src/core/BandUtils.cpp +++ b/src/core/BandUtils.cpp @@ -76,6 +76,17 @@ QString usualModeForFrequencyHz(qint64 hz) return QString(); } +bool isUkwOrUnknownHz(qint64 hz) +{ + if (hz <= 0) { + return true; // keine Frequenz -- freie Chatzeile + } + if (bandLabelForFrequencyHz(hz).isEmpty()) { + return true; // kein bekanntes Band -- nicht wegwerfen + } + return hz >= kUkwUntergrenzeHz; +} + QString bandLabelForFrequencyHz(qint64 hz) { for (const BandRange& band : kBands) { diff --git a/src/core/BandUtils.h b/src/core/BandUtils.h index 383ebac..433b279 100644 --- a/src/core/BandUtils.h +++ b/src/core/BandUtils.h @@ -20,6 +20,17 @@ namespace Contestprogramm { // antenna, and both ends need a range and a base frequency. QString bandLabelForFrequencyHz(qint64 hz); +// Die UKW-Grenze: 50 MHz und darueber (6 m, 4 m, 2 m, 70 cm, 23 cm und +// hoeher); alles darunter ist Kurzwelle. +// +// Eine Frequenz von 0 oder eine, die in kein bekanntes Band faellt, +// gilt NICHT als Kurzwelle -- eine freie Chatzeile hat keine Frequenz +// und darf nicht wegen Unkenntnis verschwinden. Dieselbe Haltung wie in +// ChatFeedModel::bandBelongsToContest: unbekanntes Band nicht heimlich +// wegwerfen. +constexpr qint64 kUkwUntergrenzeHz = 50000000LL; +bool isUkwOrUnknownHz(qint64 hz); + // The bands bandLabelForFrequencyHz() knows, lowest first. QStringList knownBands(); diff --git a/src/models/ChatFeedModel.cpp b/src/models/ChatFeedModel.cpp index d4b6ac8..1741570 100644 --- a/src/models/ChatFeedModel.cpp +++ b/src/models/ChatFeedModel.cpp @@ -302,6 +302,32 @@ ChatFeedModel::FeedLine ChatFeedModel::lineAt(int index) const // Ein Spot OHNE Frequenz (jede Chatzeile) bleibt drin: über den sagt // die Frequenz nichts aus. Und ohne bekannte Contestbänder wird nichts // aussortiert -- lieber zu viel zeigen als etwas Richtiges verschlucken. +bool ChatFeedModel::bandPassesChatFilter(qint64 freqHz) const +{ + if (!isUkwOrUnknownHz(freqHz)) { + return false; // Kurzwelle: nie im Chat + } + if (freqHz <= 0) { + return true; // freie Chatzeile + } + // Hat der aktive Contest ueberhaupt ein UKW-Band? Wenn nicht, darf + // er die UKW-Sicht nicht verengen -- sonst blendete ein + // Kurzwellen-Contest genau das aus, was der Betreiber sehen will. + bool contestHatUkwBand = false; + for (const QString& band : m_contestBands) { + qint64 lowHz = 0; + qint64 highHz = 0; + if (bandRangeHz(band, lowHz, highHz) && lowHz >= kUkwUntergrenzeHz) { + contestHatUkwBand = true; + break; + } + } + if (!contestHatUkwBand) { + return true; // ganz UKW + } + return bandBelongsToContest(freqHz); // innerhalb UKW auf die Baender verengen +} + bool ChatFeedModel::bandBelongsToContest(qint64 freqHz) const { if (freqHz <= 0 || m_contestBands.isEmpty()) { @@ -359,7 +385,7 @@ QVector ChatFeedModel::sichtbareZeilen() const QVector survivorScores; for (int i = 0; i < m_allEntries.size(); ++i) { const Entry& entry = m_allEntries.at(i); - if (entry.geo.inRange && !entry.worked && bandBelongsToContest(entry.candidate.freqHz)) { + if (entry.geo.inRange && !entry.worked && bandPassesChatFilter(entry.candidate.freqHz)) { survivorIndices.append(i); survivorScores.append(entry.score); } diff --git a/src/models/ChatFeedModel.h b/src/models/ChatFeedModel.h index 45423b4..8a1dfc6 100644 --- a/src/models/ChatFeedModel.h +++ b/src/models/ChatFeedModel.h @@ -88,6 +88,25 @@ class ChatFeedModel : public QAbstractTableModel { // Clusters haben in einem UKW-Contest nichts im Chat verloren. bool bandBelongsToContest(qint64 freqHz) const; + // Das Bandtor des Chats. Betreiber am 2026-11-04: "chat ist fuer + // mich nur ukw wichtig ... immer UKW". + // + // Kurzwelle kommt nie durch -- auch dann nicht, wenn gerade ein + // KURZWELLEN-Contest eingestellt ist. Genau das war seine Lage: + // active_contest_id = CQ_WW_SSB, active_bands = ["14"], und + // bandBelongsToContest() liess folgerichtig 20 m durch und blendete + // UKW aus. Der Chat zeigte also korrekt, was eingestellt war, und + // trotzdem das Falsche. + // + // Die Verengung auf die Contest-Baender bleibt -- aber nur + // INNERHALB von UKW. Hat der aktive Contest kein UKW-Band (oder ist + // keiner aktiv), gilt ganz UKW statt "alles". + // + // Sitzt im Sichtbarkeitsfilter, nicht am Eingang: gefiltert heisst + // verborgen, nicht weggeworfen -- die Ansicht "alle Zeilen zeigen" + // hat weiter jede Zeile (test_chat_panel verlangt das ausdruecklich). + bool bandPassesChatFilter(qint64 freqHz) const; + // Optional (nullptr = no multiplier boost, e.g. before // AppController has recomputed one yet). Non-owning; the caller // keeps it alive. Lets the importance scorer tell a still-needed diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f1d251e..6343a85 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -161,6 +161,7 @@ contestprogramm_add_test(test_next_target_suggester) contestprogramm_add_test(test_message_drafter) contestprogramm_add_test(test_weather_client) contestprogramm_add_test(test_chatfeedmodel_refresh) +contestprogramm_add_test(test_chat_nur_ukw) contestprogramm_add_test(test_recent_propagation_tracker) contestprogramm_add_test(test_color_theme) contestprogramm_add_test(test_country_borders) diff --git a/tests/test_chat_nur_ukw.cpp b/tests/test_chat_nur_ukw.cpp new file mode 100644 index 0000000..a31a47c --- /dev/null +++ b/tests/test_chat_nur_ukw.cpp @@ -0,0 +1,178 @@ +// Chat immer UKW -- Betreiber am 2026-11-04: "chat ist fuer mich nur +// ukw wichtig", auf die Rueckfrage "immer UKW". +// +// Der Befund, der dahinter steckt: in seinen Einstellungen stand +// active_contest_id = CQ_WW_SSB und active_bands = ["14"]. Der +// Chatfilter folgte diesen Baendern, liess also 20 m durch und blendete +// UKW aus. Das Programm zeigte korrekt, was eingestellt war, und +// trotzdem das Falsche -- zwei Quellen speisen den Strom, ON4KST +// (reiner UKW-Chat) und der DX-Cluster ve7cc.net (fast nur Kurzwelle). +// +// Das Tor sitzt im Sichtbarkeitsfilter, NICHT am Eingang: gefiltert +// heisst verborgen, nicht weggeworfen. test_chat_panel verlangt +// ausdruecklich, dass ein ausgefilterter Kurzwellen-Spot in der Ansicht +// "alle Zeilen zeigen" noch da ist -- ein erster Versuch, ihn in +// addCandidate() abzuweisen, hat genau daran zu Recht scheitern. + +#include + +#include +#include +#include + +#include "core/BandUtils.h" +#include "core/GeoFilter.h" +#include "core/SpotCandidate.h" +#include "data/ContestDatabase.h" +#include "data/DupeChecker.h" +#include "models/ChatFeedModel.h" + +using namespace Contestprogramm; + +namespace { +SpotCandidate spot(const QString& call, qint64 hz) +{ + SpotCandidate c; + c.callsign = call; + c.freqHz = hz; + c.timestampUtc = QDateTime::currentDateTimeUtc(); + return c; +} + +// Alle Rufzeichen, die der Filter gerade sichtbar laesst. +QStringList sichtbar(ChatFeedModel& model) +{ + QStringList raus; + for (int r = 0; r < model.rowCount(); ++r) { + raus << model.data(model.index(r, 0)).toString(); + } + return raus; +} +} // namespace + +class TestChatNurUkw : public QObject +{ + Q_OBJECT + +private slots: + void theUkwEdgeSitsAtFiftyMegahertz(); + void unknownFrequenciesAreNotThrownAway(); + void anHfContestDoesNotDragShortwaveIntoTheChat(); + void aVhfContestStillNarrowsToItsOwnBands(); + void filteredLinesAreOnlyHiddenNotDiscarded(); +}; + +void TestChatNurUkw::theUkwEdgeSitsAtFiftyMegahertz() +{ + QVERIFY(!isUkwOrUnknownHz(1810000LL)); // 160 m + QVERIFY(!isUkwOrUnknownHz(3650000LL)); // 80 m + QVERIFY(!isUkwOrUnknownHz(7050000LL)); // 40 m + QVERIFY(!isUkwOrUnknownHz(10120000LL)); // 30 m + QVERIFY(!isUkwOrUnknownHz(14080000LL)); // 20 m + QVERIFY(!isUkwOrUnknownHz(18100000LL)); // 17 m + QVERIFY(!isUkwOrUnknownHz(21200000LL)); // 15 m + QVERIFY(!isUkwOrUnknownHz(24930000LL)); // 12 m + QVERIFY(!isUkwOrUnknownHz(28400000LL)); // 10 m + QVERIFY(!isUkwOrUnknownHz(29700000LL)); // die Oberkante von 10 m + + QVERIFY(isUkwOrUnknownHz(kUkwUntergrenzeHz)); // genau die Kante + QVERIFY(isUkwOrUnknownHz(50150000LL)); // 6 m zaehlt mit + QVERIFY(isUkwOrUnknownHz(70200000LL)); // 4 m + QVERIFY(isUkwOrUnknownHz(144300000LL)); // 2 m + QVERIFY(isUkwOrUnknownHz(432200000LL)); // 70 cm + QVERIFY(isUkwOrUnknownHz(1296200000LL)); // 23 cm + QVERIFY(isUkwOrUnknownHz(10368000000LL)); // 3 cm +} + +void TestChatNurUkw::unknownFrequenciesAreNotThrownAway() +{ + QVERIFY(isUkwOrUnknownHz(0)); + QVERIFY(isUkwOrUnknownHz(-1)); + // 40 MHz liegt in keinem Amateurband -- unbekannt, nicht + // "Kurzwelle". Lieber eine Zeile zu viel als eine zu wenig. + QVERIFY(isUkwOrUnknownHz(40000000LL)); +} + +void TestChatNurUkw::anHfContestDoesNotDragShortwaveIntoTheChat() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + ContestDatabase db; + QVERIFY(db.open(dir.filePath(QStringLiteral("ukw1.sqlite")), QStringLiteral("chat_ukw_hf"))); + GeoFilter geo; + geo.setOwnGrid(QStringLiteral("JN67VV")); + geo.setRadiusKm(20000.0); + DupeChecker dupes(db); + ChatFeedModel model(geo, dupes, nullptr); + + // Genau Martins Lage am 2026-11-04. + model.setContestBands(QStringList{QStringLiteral("14")}); + + model.addCandidate(spot(QStringLiteral("XE1AOH"), 7164000LL)); // 40 m + model.addCandidate(spot(QStringLiteral("K1ABC"), 14080000LL)); // 20 m: das Contestband! + model.addCandidate(spot(QStringLiteral("OE1ABC"), 144300000LL)); // 2 m + model.addCandidate(spot(QStringLiteral("DL1DEF"), 432200000LL)); // 70 cm + model.addCandidate(spot(QStringLiteral("HB9JKL"), 0)); // freie Zeile + + const QStringList zuSehen = sichtbar(model); + QVERIFY2(!zuSehen.contains(QStringLiteral("XE1AOH")), qPrintable(zuSehen.join(QLatin1Char(',')))); + // Das eigene Contestband hilft nichts, wenn es Kurzwelle ist. + QVERIFY2(!zuSehen.contains(QStringLiteral("K1ABC")), qPrintable(zuSehen.join(QLatin1Char(',')))); + QVERIFY2(zuSehen.contains(QStringLiteral("OE1ABC")), qPrintable(zuSehen.join(QLatin1Char(',')))); + QVERIFY2(zuSehen.contains(QStringLiteral("DL1DEF")), qPrintable(zuSehen.join(QLatin1Char(',')))); + QVERIFY2(zuSehen.contains(QStringLiteral("HB9JKL")), qPrintable(zuSehen.join(QLatin1Char(',')))); +} + +void TestChatNurUkw::aVhfContestStillNarrowsToItsOwnBands() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + ContestDatabase db; + QVERIFY(db.open(dir.filePath(QStringLiteral("ukw2.sqlite")), QStringLiteral("chat_ukw_vhf"))); + GeoFilter geo; + geo.setOwnGrid(QStringLiteral("JN67VV")); + geo.setRadiusKm(20000.0); + DupeChecker dupes(db); + ChatFeedModel model(geo, dupes, nullptr); + + // Ein reiner 2-m-Contest: die Verengung innerhalb UKW bleibt, sonst + // waere die Contest-Disziplin weg. + model.setContestBands(QStringList{QStringLiteral("144")}); + + model.addCandidate(spot(QStringLiteral("OE1ABC"), 144300000LL)); // sein Band + model.addCandidate(spot(QStringLiteral("DL1DEF"), 432200000LL)); // UKW, aber nicht sein Band + model.addCandidate(spot(QStringLiteral("XE1AOH"), 7164000LL)); // Kurzwelle + + const QStringList zuSehen = sichtbar(model); + QVERIFY2(zuSehen.contains(QStringLiteral("OE1ABC")), qPrintable(zuSehen.join(QLatin1Char(',')))); + QVERIFY2(!zuSehen.contains(QStringLiteral("DL1DEF")), qPrintable(zuSehen.join(QLatin1Char(',')))); + QVERIFY2(!zuSehen.contains(QStringLiteral("XE1AOH")), qPrintable(zuSehen.join(QLatin1Char(',')))); +} + +void TestChatNurUkw::filteredLinesAreOnlyHiddenNotDiscarded() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + ContestDatabase db; + QVERIFY(db.open(dir.filePath(QStringLiteral("ukw3.sqlite")), QStringLiteral("chat_ukw_roh"))); + GeoFilter geo; + geo.setOwnGrid(QStringLiteral("JN67VV")); + geo.setRadiusKm(20000.0); + DupeChecker dupes(db); + ChatFeedModel model(geo, dupes, nullptr); + model.setContestBands(QStringList{QStringLiteral("144")}); + + model.addCandidate(spot(QStringLiteral("XE1AOH"), 7164000LL)); + model.addCandidate(spot(QStringLiteral("OE1ABC"), 144300000LL)); + + QVERIFY(!sichtbar(model).contains(QStringLiteral("XE1AOH"))); + // Beide Zeilen liegen weiter im Strom ... + QCOMPARE(model.lineCount(), 2); + // ... und "alle Zeilen zeigen" holt die Kurzwelle wieder hervor. + model.setShowRawFeed(true); + QVERIFY2(sichtbar(model).contains(QStringLiteral("XE1AOH")), + "der Kurzwellen-Spot ist ganz verschwunden"); +} + +QTEST_MAIN(TestChatNurUkw) +#include "test_chat_nur_ukw.moc"