From 1223ef9a35cbf6d3c396a93620d0305528635720 Mon Sep 17 00:00:00 2001 From: aa5sh <84428382+aa5sh@users.noreply.github.com> Date: Sun, 10 Aug 2025 17:46:31 -0500 Subject: [PATCH 1/8] ClubLog CTY.XML Due to it's very detailed history of exceptions and invalid operations as well as date ranges to match prefixes I added to be used for importing of old logs as wells when in manual entry mode. --- QLog.pro | 6 +- core/LOVDownloader.cpp | 268 +++++++++++++++++++++++++++++++++++++- core/LOVDownloader.h | 11 +- core/Migration.cpp | 20 +-- core/Migration.h | 2 +- data/Data.cpp | 152 +++++++++++++++++++++ data/Data.h | 1 + logformat/LogFormat.cpp | 2 +- res/res.qrc | 1 + res/sql/migration_035.sql | 61 +++++++++ ui/NewContactWidget.cpp | 8 +- 11 files changed, 515 insertions(+), 17 deletions(-) create mode 100644 res/sql/migration_035.sql diff --git a/QLog.pro b/QLog.pro index c904102c..9d2186eb 100644 --- a/QLog.pro +++ b/QLog.pro @@ -11,7 +11,7 @@ greaterThan(QT_MAJOR_VERSION, 5): QT += widgets TARGET = qlog TEMPLATE = app VERSION = 0.45.0 - +LIBS += -lz DEFINES += VERSION=\\\"$$VERSION\\\" # Define paths to HAMLIB. Leave empty if system libraries should be used @@ -476,8 +476,8 @@ macx: { INSTALLS += target } - INCLUDEPATH += /usr/local/include /opt/homebrew/include - LIBS += -L/usr/local/lib -L/opt/homebrew/lib -lhamlib -lsqlite3 + INCLUDEPATH += /usr/local/include /opt/homebrew/include /opt/local/include + LIBS += -L/usr/local/lib -L/opt/homebrew/lib -lhamlib -lsqlite3 -L/opt/local/lib equals(QT_MAJOR_VERSION, 6): LIBS += -lqt6keychain equals(QT_MAJOR_VERSION, 5): LIBS += -lqt5keychain DISTFILES += diff --git a/core/LOVDownloader.cpp b/core/LOVDownloader.cpp index 4311d200..f86ad6fb 100644 --- a/core/LOVDownloader.cpp +++ b/core/LOVDownloader.cpp @@ -16,6 +16,7 @@ #include "LOVDownloader.h" #include "debug.h" +#include MODULE_IDENTIFICATION("qlog.core.lovdownloader"); @@ -192,7 +193,9 @@ void LOVDownloader::parseData(const SourceDefinition &sourceDef, QTextStream &da case MEMBERSHIPCONTENTLIST: parseMembershipContent(sourceDef, data); break; - + case CLUBLOGCTY: + parseClubLogCTY(sourceDef, data); + break; default: qWarning() << "Unssorted type to download" << sourceDef.type << sourceDef.fileName; } @@ -1081,6 +1084,11 @@ void LOVDownloader::processReply(QNetworkReply *reply) QFile file(dir.filePath(sourceDef.fileName)); file.open(QIODevice::WriteOnly); + if (sourceType == CLUBLOGCTY) + { + QByteArray maybeXml = gunzip(data); + if (!maybeXml.isEmpty()) data = maybeXml; + } file.write(data); file.flush(); file.close(); @@ -1098,3 +1106,261 @@ void LOVDownloader::processReply(QNetworkReply *reply) emit finished(false); } } + + +QByteArray LOVDownloader::gunzip(const QByteArray &in) { + if (in.isEmpty()) return {}; + z_stream strm{}; + strm.next_in = reinterpret_cast(const_cast(in.data())); + strm.avail_in = in.size(); + // 16 + MAX_WBITS tells zlib to parse gzip header/footer + if (inflateInit2(&strm, 16 + MAX_WBITS) != Z_OK) return {}; + QByteArray out; + char buf[8192]; + int ret = Z_OK; + while (ret == Z_OK) { + strm.next_out = reinterpret_cast(buf); + strm.avail_out = sizeof(buf); + ret = inflate(&strm, Z_NO_FLUSH); + if (ret == Z_OK || ret == Z_STREAM_END) { + out.append(buf, sizeof(buf) - strm.avail_out); + } + } + inflateEnd(&strm); + return out; +} + +#include +#include +#include + +void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStream &stream) +{ + FCT_IDENTIFICATION; + + if (sourceDef.type != CLUBLOGCTY) + { + return; + } + + // Read whole text (it’s XML); QXmlStreamReader can also take QIODevice, but we + // already have a QTextStream here. + const QString xmlText = stream.readAll(); + QXmlStreamReader xml(xmlText); + + // Clean all five tables inside one transaction + QSqlDatabase::database().transaction(); + auto rollback = [&](){ + qCWarning(runtime) << "ClubLog CTY import failed - rollback"; + QSqlDatabase::database().rollback(); + }; + + if (!deleteTable("clublog_zone_exceptions") + || !deleteTable("clublog_invalid_ops") + || !deleteTable("clublog_exceptions") + || !deleteTable("clublog_prefixes") + || !deleteTable("clublog_entities")) { + rollback(); + return; + } + + QSqlQuery insEntity, insPrefix, insExc, insInv, insZone; + + // prepared statements + if (!insEntity.prepare( + "INSERT INTO clublog_entities(adif,name,prefix,deleted,cqz,cont,lon,lat,start,\"end\",whitelist,whitelist_start,whitelist_end)" + "VALUES(:adif,:name,:prefix,:deleted,:cqz,:cont,:lon,:lat,:start,:end,:whitelist,:whitelist_start,:whitelist_end)")) + { qWarning() << insEntity.lastError(); rollback(); return; } + + if (!insPrefix.prepare( + "INSERT INTO clublog_prefixes(record,call,entity,adif,cqz,cont,lon,lat,start,\"end\")" + "VALUES(:record,:call,:entity,:adif,:cqz,:cont,:lon,:lat,:start,:end)")) + { qWarning() << insPrefix.lastError(); rollback(); return; } + + if (!insExc.prepare( + "INSERT INTO clublog_exceptions(record,call,entity,adif,cqz,cont,lon,lat,start,\"end\")" + "VALUES(:record,:call,:entity,:adif,:cqz,:cont,:lon,:lat,:start,:end)")) + { qWarning() << insExc.lastError(); rollback(); return; } + + if (!insInv.prepare( + "INSERT INTO clublog_invalid_ops(record,call,start,\"end\")" + "VALUES(:record,:call,:start,:end)")) + { qWarning() << insInv.lastError(); rollback(); return; } + + if (!insZone.prepare( + "INSERT INTO clublog_zone_exceptions(record,call,zone,start,\"end\")" + "VALUES(:record,:call,:zone,:start,:end)")) + { qWarning() << insZone.lastError(); rollback(); return; } + + auto readText = [&](QXmlStreamReader &x)->QString { return x.readElementText().trimmed(); }; + auto readDate = [&](const QString &s)->QString { return s; }; // store ISO8601 text as-is + + // Cursor down to + while (!xml.atEnd() && !(xml.isStartElement() && xml.name() == "clublog")) xml.readNext(); + + if (xml.atEnd()) { + qWarning() << "ClubLog: not found"; + rollback(); return; + } + + // Iterate children of + while (!xml.atEnd()) { + xml.readNext(); + + if (!xml.isStartElement()) continue; + + const QString top = xml.name().toString(); + + if (top == "entities") { + // ...... + while (!(xml.isEndElement() && xml.name() == "entities")) { + xml.readNext(); + if (xml.isStartElement() && xml.name() == "entity") { + // parse one entity + int adif = 0; QString name, prefix, cont; bool deleted=false; + int cqz = 0; QString start, end, whitelist_start, whitelist_end; bool whitelist=false; + double lon=NAN, lat=NAN; + + while (!(xml.isEndElement() && xml.name() == "entity")) { + xml.readNext(); + if (!xml.isStartElement()) continue; + const QString tag = xml.name().toString(); + if (tag=="adif") adif = readText(xml).toInt(); + else if (tag=="name") name = readText(xml); + else if (tag=="prefix") prefix = readText(xml); + else if (tag=="deleted") deleted = (readText(xml).compare("true", Qt::CaseInsensitive)==0); + else if (tag=="cqz") cqz = readText(xml).toInt(); + else if (tag=="cont") cont = readText(xml); + else if (tag=="long") lon = readText(xml).toDouble(); + else if (tag=="lat") lat = readText(xml).toDouble(); + else if (tag=="start") start = readDate(readText(xml)); + else if (tag=="end") end = readDate(readText(xml)); + else if (tag=="whitelist") whitelist = (readText(xml).compare("true", Qt::CaseInsensitive)==0); + else if (tag=="whitelist_start") whitelist_start = readDate(readText(xml)); + else if (tag=="whitelist_end") whitelist_end = readDate(readText(xml)); + else xml.skipCurrentElement(); + } + + insEntity.bindValue(":adif", adif); + insEntity.bindValue(":name", name); + insEntity.bindValue(":prefix", prefix); + insEntity.bindValue(":deleted", deleted ? 1 : 0); + insEntity.bindValue(":cqz", cqz ? cqz : QVariant(QVariant::Int)); + insEntity.bindValue(":cont", cont.isEmpty()? QVariant(QVariant::String) : cont); + insEntity.bindValue(":lon", std::isnan(lon) ? QVariant(QVariant::Double) : lon); + insEntity.bindValue(":lat", std::isnan(lat) ? QVariant(QVariant::Double) : lat); + insEntity.bindValue(":start", start.isEmpty()? QVariant(QVariant::String) : start); + insEntity.bindValue(":end", end.isEmpty()? QVariant(QVariant::String) : end); + insEntity.bindValue(":whitelist", whitelist?1:0); + insEntity.bindValue(":whitelist_start", whitelist_start.isEmpty()? QVariant(QVariant::String):whitelist_start); + insEntity.bindValue(":whitelist_end", whitelist_end.isEmpty()? QVariant(QVariant::String):whitelist_end); + + if (!insEntity.exec()) { qWarning() << insEntity.lastError(); rollback(); return; } + } + } + } + else if (top == "prefixes" || top == "exceptions") { + // these two share the same internal structure: ... + bool isPrefix = (top=="prefixes"); + QSqlQuery &ins = isPrefix ? insPrefix : insExc; + + while (!(xml.isEndElement() && xml.name() == top)) { + xml.readNext(); + if (xml.isStartElement() && (xml.name()=="prefix" || xml.name()=="exception")) { + bool ok=false; + quint32 rec = xml.attributes().value("record").toUInt(&ok); + QString call, entity, cont, start, end; + int adif=0, cqz=0; double lon=NAN, lat=NAN; + + while (!(xml.isEndElement() && (xml.name()=="prefix" || xml.name()=="exception"))) { + xml.readNext(); + if (!xml.isStartElement()) continue; + const QString tag = xml.name().toString(); + if (tag=="call") call = readText(xml); + else if (tag=="entity") entity = readText(xml); + else if (tag=="adif") adif = readText(xml).toInt(); + else if (tag=="cqz") cqz = readText(xml).toInt(); + else if (tag=="cont") cont = readText(xml); + else if (tag=="long") lon = readText(xml).toDouble(); + else if (tag=="lat") lat = readText(xml).toDouble(); + else if (tag=="start") start = readDate(readText(xml)); + else if (tag=="end") end = readDate(readText(xml)); + else xml.skipCurrentElement(); + } + + ins.bindValue(":record", rec); + ins.bindValue(":call", call); + ins.bindValue(":entity", entity); + ins.bindValue(":adif", adif); + ins.bindValue(":cqz", cqz?cqz:QVariant(QVariant::Int)); + ins.bindValue(":cont", cont.isEmpty()? QVariant(QVariant::String) : cont); + ins.bindValue(":lon", std::isnan(lon) ? QVariant(QVariant::Double) : lon); + ins.bindValue(":lat", std::isnan(lat) ? QVariant(QVariant::Double) : lat); + ins.bindValue(":start", start.isEmpty()? QVariant(QVariant::String) : start); + ins.bindValue(":end", end.isEmpty()? QVariant(QVariant::String) : end); + + if (!ins.exec()) { qWarning() << ins.lastError(); rollback(); return; } + } + } + } + else if (top == "invalid_operations") { + while (!(xml.isEndElement() && xml.name()=="invalid_operations")) { + xml.readNext(); + if (xml.isStartElement() && xml.name()=="invalid") { + bool ok=false; quint32 rec = xml.attributes().value("record").toUInt(&ok); + QString call, start, end; + while (!(xml.isEndElement() && xml.name()=="invalid")) { + xml.readNext(); + if (!xml.isStartElement()) continue; + const QString tag = xml.name().toString(); + if (tag=="call") call = readText(xml); + else if (tag=="start") start = readDate(readText(xml)); + else if (tag=="end") end = readDate(readText(xml)); + else xml.skipCurrentElement(); + } + insInv.bindValue(":record", rec); + insInv.bindValue(":call", call); + insInv.bindValue(":start", start.isEmpty()? QVariant(QVariant::String) : start); + insInv.bindValue(":end", end.isEmpty()? QVariant(QVariant::String) : end); + if (!insInv.exec()) { qWarning() << insInv.lastError(); rollback(); return; } + } + } + } + else if (top == "zone_exceptions") { + while (!(xml.isEndElement() && xml.name()=="zone_exceptions")) { + xml.readNext(); + if (xml.isStartElement() && xml.name()=="zone_exception") { + bool ok=false; quint32 rec = xml.attributes().value("record").toUInt(&ok); + QString call, start, end; int zone=0; + while (!(xml.isEndElement() && xml.name()=="zone_exception")) { + xml.readNext(); + if (!xml.isStartElement()) continue; + const QString tag = xml.name().toString(); + if (tag=="call") call = readText(xml); + else if (tag=="zone") zone = readText(xml).toInt(); + else if (tag=="start") start = readDate(readText(xml)); + else if (tag=="end") end = readDate(readText(xml)); + else xml.skipCurrentElement(); + } + insZone.bindValue(":record", rec); + insZone.bindValue(":call", call); + insZone.bindValue(":zone", zone); + insZone.bindValue(":start", start.isEmpty()? QVariant(QVariant::String) : start); + insZone.bindValue(":end", end.isEmpty()? QVariant(QVariant::String) : end); + if (!insZone.exec()) { qWarning() << insZone.lastError(); rollback(); return; } + } + } + } + else { + xml.skipCurrentElement(); + } + } + + if (xml.hasError()) { + qWarning() << "ClubLog XML error:" << xml.errorString(); + rollback(); return; + } + + QSqlDatabase::database().commit(); + qCDebug(runtime) << "ClubLog CTY import finished."; +} diff --git a/core/LOVDownloader.h b/core/LOVDownloader.h index 15a632cf..dccf6539 100644 --- a/core/LOVDownloader.h +++ b/core/LOVDownloader.h @@ -19,7 +19,8 @@ class LOVDownloader : public QObject IOTALIST = 4, POTADIRECTORY = 5, MEMBERSHIPCONTENTLIST = 6, - UNDEF = 7 + CLUBLOGCTY = 7, + UNDEF = 8 }; public: @@ -103,6 +104,12 @@ public slots: "content.csv", "LOV/last_membershipcontent_update", "membership_directory", + 7)}, + {CLUBLOGCTY, SourceDefinition(CLUBLOGCTY, + "https://cdn.clublog.org/cty.php?api=a1a2215eea4990661a8ce55873a33c4ef290b49d", + "clublog_cty.xml", + "LOV/last_clublogcty_update", + "clublog_entities", 7)} }; @@ -126,6 +133,8 @@ public slots: void parseIOTA(const SourceDefinition &sourceDef, QTextStream& data); void parsePOTA(const SourceDefinition &sourceDef, QTextStream& data); void parseMembershipContent(const SourceDefinition &sourceDef, QTextStream& data); + static QByteArray gunzip(const QByteArray &in); + void parseClubLogCTY(const SourceDefinition &sourceDef, QTextStream &data); private slots: void processReply(QNetworkReply*); diff --git a/core/Migration.cpp b/core/Migration.cpp index 00efaf54..c57ea8a5 100644 --- a/core/Migration.cpp +++ b/core/Migration.cpp @@ -354,14 +354,14 @@ bool Migration::updateExternalResource() connect(&progress, &QProgressDialog::canceled, &downloader, &LOVDownloader::abortRequest); - updateExternalResourceProgress(progress, downloader, LOVDownloader::CTY, "(1/7)"); - updateExternalResourceProgress(progress, downloader, LOVDownloader::SATLIST, "(2/7)"); - updateExternalResourceProgress(progress, downloader, LOVDownloader::SOTASUMMITS, "(3/7)"); - updateExternalResourceProgress(progress, downloader, LOVDownloader::WWFFDIRECTORY, "(4/7)"); - updateExternalResourceProgress(progress, downloader, LOVDownloader::IOTALIST, "(5/7)"); - updateExternalResourceProgress(progress, downloader, LOVDownloader::POTADIRECTORY, "(6/7)"); - updateExternalResourceProgress(progress, downloader, LOVDownloader::MEMBERSHIPCONTENTLIST, "(7/7)"); - + updateExternalResourceProgress(progress, downloader, LOVDownloader::CTY, "(1/8)"); + updateExternalResourceProgress(progress, downloader, LOVDownloader::SATLIST, "(2/8)"); + updateExternalResourceProgress(progress, downloader, LOVDownloader::SOTASUMMITS, "(3/8)"); + updateExternalResourceProgress(progress, downloader, LOVDownloader::WWFFDIRECTORY, "(4/8)"); + updateExternalResourceProgress(progress, downloader, LOVDownloader::IOTALIST, "(5/8)"); + updateExternalResourceProgress(progress, downloader, LOVDownloader::POTADIRECTORY, "(6/8)"); + updateExternalResourceProgress(progress, downloader, LOVDownloader::MEMBERSHIPCONTENTLIST, "(7/8)"); + updateExternalResourceProgress(progress, downloader, LOVDownloader::CLUBLOGCTY, "8/8"); return true; } @@ -398,7 +398,9 @@ void Migration::updateExternalResourceProgress(QProgressDialog& progress, case LOVDownloader::SourceType::MEMBERSHIPCONTENTLIST: stringInfo = tr("Membership Directory Records"); break; - + case LOVDownloader::SourceType::CLUBLOGCTY: + stringInfo = tr("Clublog CTY.XML"); + break; default: stringInfo = tr("List of Values"); } diff --git a/core/Migration.h b/core/Migration.h index 2addb2eb..652dc3ea 100644 --- a/core/Migration.h +++ b/core/Migration.h @@ -42,7 +42,7 @@ class Migration : public QObject QString fixIntlField(QSqlQuery &query, const QString &columName, const QString &columnNameIntl); bool refreshUploadStatusTrigger(); - static const int latestVersion = 34; + static const int latestVersion = 35; }; #endif // QLOG_CORE_MIGRATION_H diff --git a/data/Data.cpp b/data/Data.cpp index b85e0c1d..ae1cedcd 100644 --- a/data/Data.cpp +++ b/data/Data.cpp @@ -1092,6 +1092,158 @@ DxccEntity Data::lookupDxcc(const QString &callsign) return dxccRet; } +// Data.cpp +// Data.cpp +DxccEntity Data::lookupCallsign(const QString& callsign, const QDateTime& date) +{ + const QDateTime useDate = date.isValid() ? date : QDateTime::currentDateTimeUtc(); + const QString dateIso = useDate.toUTC().toString(Qt::ISODate); + + QString lookupPrefix = callsign; // use the callsign with optional prefix as default to find the dxcc + const Callsign parsedCallsign(callsign); // use Callsign to split the callsign into its parts + + if ( parsedCallsign.isValid() ) + { + QString suffix = parsedCallsign.getSuffix(); + if ( suffix.length() == 1 ) // some countries add single numbers as suffix to designate a call area, e.g. /4 + { + bool isNumber = false; + (void)suffix.toInt(&isNumber); + if ( isNumber ) + { + lookupPrefix = parsedCallsign.getBasePrefix() + suffix; // use the call prefix and the number from the suffix to find the dxcc + } + } + else if ( suffix.length() > 1 + && !parsedCallsign.secondarySpecialSuffixes.contains(suffix) ) // if there is more than one character and it is not one of the special suffixes, we definitely have a call prefix as suffix + { + lookupPrefix = suffix + "/" + parsedCallsign.getBase(); + qWarning() << suffix << lookupPrefix << callsign; + } + } + + + + DxccEntity e; // empty = no match / invalid + QSqlQuery q; + e.dxcc = 0; + e.ituz = 0; + e.cqz = 0; + e.tz = 0; + + if(callsign.endsWith("/MM")) return e; + + // 1) Invalid ops ⇒ treat as no result + q.prepare( + "SELECT 1 FROM clublog_invalid_ops " + "WHERE UPPER(call)=UPPER(?) " + "AND (start IS NULL OR start <= ?) " + "AND (end IS NULL OR end >= ?) " + "LIMIT 1"); + q.addBindValue(lookupPrefix); + q.addBindValue(dateIso); + q.addBindValue(dateIso); + if (q.exec() && q.next()) + { + qWarning() << "Invalid Operation" << callsign; + return e; + } + + auto applyZoneOverride = [&](DxccEntity& out){ + QSqlQuery qz; + qz.prepare( + "SELECT zone FROM clublog_zone_exceptions " + "WHERE UPPER(call)=UPPER(?) " + "AND (start IS NULL OR start <= ?) " + "AND (end IS NULL OR end >= ?) " + "ORDER BY record DESC LIMIT 1"); + qz.addBindValue(lookupPrefix); + qz.addBindValue(dateIso); + qz.addBindValue(dateIso); + if (qz.exec() && qz.next()) + out.cqz = qz.value(0).toInt(); + }; + + // 2) Exact exceptions (join dxcc_entities to get ituz/tz) + { + QSqlQuery qe; + qe.prepare( + "SELECT " + " e.name, e.prefix, e.adif, " + " COALESCE(x.cont, e.cont), " + " COALESCE(x.cqz , e.cqz ), " + " COALESCE(x.lat , e.lat ), " + " COALESCE(x.lon, e.lon), " + " de.ituz, de.tz " + "FROM clublog_exceptions x " + "JOIN clublog_entities e ON e.adif = x.adif " + "LEFT JOIN dxcc_entities de ON de.id = e.adif " + "WHERE UPPER(x.call)=UPPER(?) " + "AND (x.start IS NULL OR x.start <= ?) " + "AND (x.end IS NULL OR x.end >= ?) " + "ORDER BY x.record DESC LIMIT 1"); + qe.addBindValue(lookupPrefix); + qe.addBindValue(dateIso); + qe.addBindValue(dateIso); + + if (qe.exec() && qe.next()) { + DxccEntity out; + out.country = qe.value(0).toString(); + out.prefix = qe.value(1).toString(); + out.dxcc = qe.value(2).toInt(); + out.cont = qe.value(3).toString(); + out.cqz = qe.value(4).toInt(); + out.latlon[0] = qe.value(5).toDouble(); + out.latlon[1] = qe.value(6).toDouble(); + out.ituz = qe.value(7).isNull() ? 0 : qe.value(7).toInt(); + out.tz = qe.value(8).isNull() ? 0.0f : static_cast(qe.value(8).toDouble()); + out.flag = qe.value(1).toString(); + applyZoneOverride(out); + return out; + } + } + + // 3) Longest prefix (join dxcc_entities to get ituz/tz) + { + QSqlQuery qp; + qp.prepare( + "SELECT " + " e.name, e.prefix, e.adif, e.cont, e.cqz, e.lat, e.lon, " + " de.ituz, de.tz " + "FROM clublog_prefixes p " + "JOIN clublog_entities e ON e.adif = p.adif " + "LEFT JOIN dxcc_entities de ON de.id = e.adif " + "WHERE ? LIKE p.call || '%' " + "AND (p.start IS NULL OR p.start <= ?) " + "AND (p.end IS NULL OR p.end >= ?) " + "ORDER BY length(p.call) DESC, p.record DESC " + "LIMIT 1"); + qp.addBindValue(lookupPrefix); + qp.addBindValue(dateIso); + qp.addBindValue(dateIso); + + if (qp.exec() && qp.next()) { + DxccEntity out; + out.country = qp.value(0).toString(); + out.prefix = qp.value(1).toString(); + out.dxcc = qp.value(2).toInt(); + out.cont = qp.value(3).toString(); + out.cqz = qp.value(4).toInt(); + out.latlon[0] = qp.value(5).toDouble(); + out.latlon[1] = qp.value(6).toDouble(); + out.ituz = qp.value(7).isNull() ? 0 : qp.value(7).toInt(); + out.tz = qp.value(8).isNull() ? 0.0f : static_cast(qp.value(8).toDouble()); + out.flag = qp.value(1).toString(); + applyZoneOverride(out); + return out; + } + } + + // 4) No match + return e; +} + + DxccEntity Data::lookupDxccID(const int dxccID) { FCT_IDENTIFICATION; diff --git a/data/Data.h b/data/Data.h index a50f14f8..0d0d2b5d 100644 --- a/data/Data.h +++ b/data/Data.h @@ -160,6 +160,7 @@ class Data : public QObject QStringList potaIDList() { return potaRefID.keys();} QString getIANATimeZone(double, double); QStringList sigIDList(); + DxccEntity lookupCallsign(const QString& callsign, const QDateTime& date); signals: diff --git a/logformat/LogFormat.cpp b/logformat/LogFormat.cpp index 9222f0cb..4a871a7c 100644 --- a/logformat/LogFormat.cpp +++ b/logformat/LogFormat.cpp @@ -499,7 +499,7 @@ unsigned long LogFormat::runImport(QTextStream& importLogStream, if ( recordDXCCId != 0 || updateDxcc ) { - const DxccEntity &entity = ( updateDxcc ) ? Data::instance()->lookupDxcc(call.toString()) + const DxccEntity &entity = ( updateDxcc ) ? Data::instance()->lookupCallsign(call.toString(),start_time) : Data::instance()->lookupDxccID(recordDXCCId); if ( entity.dxcc == 0 ) // DXCC not found diff --git a/res/res.qrc b/res/res.qrc index f961a7da..2bd2a3c5 100644 --- a/res/res.qrc +++ b/res/res.qrc @@ -47,5 +47,6 @@ sql/migration_032.sql sql/migration_033.sql sql/migration_034.sql + sql/migration_035.sql diff --git a/res/sql/migration_035.sql b/res/sql/migration_035.sql new file mode 100644 index 00000000..143b4bab --- /dev/null +++ b/res/sql/migration_035.sql @@ -0,0 +1,61 @@ +CREATE TABLE IF NOT EXISTS clublog_entities ( + adif INTEGER PRIMARY KEY, + name TEXT NOT NULL, + prefix TEXT NOT NULL, + deleted INTEGER NOT NULL, + cqz INTEGER, + cont TEXT, + lon REAL, + lat REAL, + start TEXT, + "end" TEXT, + whitelist INTEGER, + whitelist_start TEXT, + whitelist_end TEXT +); + +CREATE TABLE IF NOT EXISTS clublog_exceptions ( + record INTEGER PRIMARY KEY, + call TEXT NOT NULL, + entity TEXT NOT NULL, + adif INTEGER NOT NULL, + cqz INTEGER, + cont TEXT, + lon REAL, + lat REAL, + start TEXT, + "end" TEXT +); +CREATE INDEX IF NOT EXISTS idx_exceptions_call ON clublog_exceptions(call); + +CREATE TABLE IF NOT EXISTS clublog_prefixes ( + record INTEGER PRIMARY KEY, + call TEXT NOT NULL, + entity TEXT NOT NULL, + adif INTEGER NOT NULL, + cqz INTEGER, + cont TEXT, + lon REAL, + lat REAL, + start TEXT, + end TEXT +); +CREATE INDEX IF NOT EXISTS idx_prefixes_call ON clublog_prefixes(call); + +CREATE TABLE IF NOT EXISTS clublog_invalid_ops ( + record INTEGER PRIMARY KEY, + call TEXT NOT NULL, + start TEXT, + end TEXT +); +CREATE INDEX IF NOT EXISTS idx_invalid_call ON clublog_invalid_ops(call); + +CREATE TABLE IF NOT EXISTS clublog_zone_exceptions ( + record INTEGER PRIMARY KEY, + call TEXT NOT NULL, + zone INTEGER NOT NULL, + start TEXT, + end TEXT +); +CREATE INDEX IF NOT EXISTS idx_zone_call ON clublog_zone_exceptions(call); + diff --git a/ui/NewContactWidget.cpp b/ui/NewContactWidget.cpp index 866c0605..df0fe04a 100644 --- a/ui/NewContactWidget.cpp +++ b/ui/NewContactWidget.cpp @@ -524,7 +524,13 @@ void NewContactWidget::setDxccInfo(const QString &callsign) qCDebug(function_parameters) << callsign; - setDxccInfo(Data::instance()->lookupDxcc(callsign.toUpper())); + if(isManualEnterMode) + { + QDateTime qsoDt = QDateTime(ui->dateEdit->date(),ui->timeOnEdit->time()); + setDxccInfo(Data::instance()->lookupCallsign(callsign.toUpper(), qsoDt.toUTC())); + } + else + setDxccInfo(Data::instance()->lookupDxcc(callsign.toUpper())); } void NewContactWidget::useFieldsFromPrevQSO(const QString &callsign, const QString &grid) From f650058cf92ac8ae077bf721e483dcfb086d6ab0 Mon Sep 17 00:00:00 2001 From: aa5sh <84428382+aa5sh@users.noreply.github.com> Date: Sun, 10 Aug 2025 18:30:05 -0500 Subject: [PATCH 2/8] Some Cleanup to try fix Compile errors on GitHub --- core/LOVDownloader.cpp | 38 ++++++++++---------- data/Data.cpp | 82 +++++++++++++++++++++++++----------------- 2 files changed, 69 insertions(+), 51 deletions(-) diff --git a/core/LOVDownloader.cpp b/core/LOVDownloader.cpp index f86ad6fb..66b4922a 100644 --- a/core/LOVDownloader.cpp +++ b/core/LOVDownloader.cpp @@ -1219,7 +1219,7 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre // parse one entity int adif = 0; QString name, prefix, cont; bool deleted=false; int cqz = 0; QString start, end, whitelist_start, whitelist_end; bool whitelist=false; - double lon=NAN, lat=NAN; + double lon=0.0, lat=0.0; while (!(xml.isEndElement() && xml.name() == "entity")) { xml.readNext(); @@ -1245,15 +1245,15 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre insEntity.bindValue(":name", name); insEntity.bindValue(":prefix", prefix); insEntity.bindValue(":deleted", deleted ? 1 : 0); - insEntity.bindValue(":cqz", cqz ? cqz : QVariant(QVariant::Int)); - insEntity.bindValue(":cont", cont.isEmpty()? QVariant(QVariant::String) : cont); - insEntity.bindValue(":lon", std::isnan(lon) ? QVariant(QVariant::Double) : lon); - insEntity.bindValue(":lat", std::isnan(lat) ? QVariant(QVariant::Double) : lat); - insEntity.bindValue(":start", start.isEmpty()? QVariant(QVariant::String) : start); - insEntity.bindValue(":end", end.isEmpty()? QVariant(QVariant::String) : end); + insEntity.bindValue(":cqz", cqz ? cqz : 0); + insEntity.bindValue(":cont", cont.isEmpty()? "" : cont); + insEntity.bindValue(":lon", lon); + insEntity.bindValue(":lat", lat); + insEntity.bindValue(":start", start.isEmpty()? "" : start); + insEntity.bindValue(":end", end.isEmpty()? "" : end); insEntity.bindValue(":whitelist", whitelist?1:0); - insEntity.bindValue(":whitelist_start", whitelist_start.isEmpty()? QVariant(QVariant::String):whitelist_start); - insEntity.bindValue(":whitelist_end", whitelist_end.isEmpty()? QVariant(QVariant::String):whitelist_end); + insEntity.bindValue(":whitelist_start", whitelist_start.isEmpty()? "":whitelist_start); + insEntity.bindValue(":whitelist_end", whitelist_end.isEmpty()? "":whitelist_end); if (!insEntity.exec()) { qWarning() << insEntity.lastError(); rollback(); return; } } @@ -1292,12 +1292,12 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre ins.bindValue(":call", call); ins.bindValue(":entity", entity); ins.bindValue(":adif", adif); - ins.bindValue(":cqz", cqz?cqz:QVariant(QVariant::Int)); - ins.bindValue(":cont", cont.isEmpty()? QVariant(QVariant::String) : cont); - ins.bindValue(":lon", std::isnan(lon) ? QVariant(QVariant::Double) : lon); - ins.bindValue(":lat", std::isnan(lat) ? QVariant(QVariant::Double) : lat); - ins.bindValue(":start", start.isEmpty()? QVariant(QVariant::String) : start); - ins.bindValue(":end", end.isEmpty()? QVariant(QVariant::String) : end); + ins.bindValue(":cqz", cqz?cqz:0); + ins.bindValue(":cont", cont.isEmpty()? "" : cont); + ins.bindValue(":lon", lon); + ins.bindValue(":lat", lat); + ins.bindValue(":start", start.isEmpty()? "" : start); + ins.bindValue(":end", end.isEmpty()? "" : end); if (!ins.exec()) { qWarning() << ins.lastError(); rollback(); return; } } @@ -1320,8 +1320,8 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre } insInv.bindValue(":record", rec); insInv.bindValue(":call", call); - insInv.bindValue(":start", start.isEmpty()? QVariant(QVariant::String) : start); - insInv.bindValue(":end", end.isEmpty()? QVariant(QVariant::String) : end); + insInv.bindValue(":start", start); + insInv.bindValue(":end", end); if (!insInv.exec()) { qWarning() << insInv.lastError(); rollback(); return; } } } @@ -1345,8 +1345,8 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre insZone.bindValue(":record", rec); insZone.bindValue(":call", call); insZone.bindValue(":zone", zone); - insZone.bindValue(":start", start.isEmpty()? QVariant(QVariant::String) : start); - insZone.bindValue(":end", end.isEmpty()? QVariant(QVariant::String) : end); + insZone.bindValue(":start", start); + insZone.bindValue(":end", end); if (!insZone.exec()) { qWarning() << insZone.lastError(); rollback(); return; } } } diff --git a/data/Data.cpp b/data/Data.cpp index ae1cedcd..1fca0b02 100644 --- a/data/Data.cpp +++ b/data/Data.cpp @@ -1118,12 +1118,9 @@ DxccEntity Data::lookupCallsign(const QString& callsign, const QDateTime& date) && !parsedCallsign.secondarySpecialSuffixes.contains(suffix) ) // if there is more than one character and it is not one of the special suffixes, we definitely have a call prefix as suffix { lookupPrefix = suffix + "/" + parsedCallsign.getBase(); - qWarning() << suffix << lookupPrefix << callsign; } } - - DxccEntity e; // empty = no match / invalid QSqlQuery q; e.dxcc = 0; @@ -1137,15 +1134,15 @@ DxccEntity Data::lookupCallsign(const QString& callsign, const QDateTime& date) q.prepare( "SELECT 1 FROM clublog_invalid_ops " "WHERE UPPER(call)=UPPER(?) " - "AND (start IS NULL OR start <= ?) " - "AND (end IS NULL OR end >= ?) " + "AND (start IS '' OR start <= ?) " + "AND (end IS '' OR end >= ?) " "LIMIT 1"); q.addBindValue(lookupPrefix); q.addBindValue(dateIso); q.addBindValue(dateIso); if (q.exec() && q.next()) { - qWarning() << "Invalid Operation" << callsign; + qDebug() << "Invalid Operation" << callsign; return e; } @@ -1154,8 +1151,8 @@ DxccEntity Data::lookupCallsign(const QString& callsign, const QDateTime& date) qz.prepare( "SELECT zone FROM clublog_zone_exceptions " "WHERE UPPER(call)=UPPER(?) " - "AND (start IS NULL OR start <= ?) " - "AND (end IS NULL OR end >= ?) " + "AND (start IS '' OR start <= ?) " + "AND (end IS '' OR end >= ?) " "ORDER BY record DESC LIMIT 1"); qz.addBindValue(lookupPrefix); qz.addBindValue(dateIso); @@ -1179,8 +1176,8 @@ DxccEntity Data::lookupCallsign(const QString& callsign, const QDateTime& date) "JOIN clublog_entities e ON e.adif = x.adif " "LEFT JOIN dxcc_entities de ON de.id = e.adif " "WHERE UPPER(x.call)=UPPER(?) " - "AND (x.start IS NULL OR x.start <= ?) " - "AND (x.end IS NULL OR x.end >= ?) " + "AND (x.start IS '' OR x.start <= ?) " + "AND (x.end IS '' OR x.end >= ?) " "ORDER BY x.record DESC LIMIT 1"); qe.addBindValue(lookupPrefix); qe.addBindValue(dateIso); @@ -1197,13 +1194,16 @@ DxccEntity Data::lookupCallsign(const QString& callsign, const QDateTime& date) out.latlon[1] = qe.value(6).toDouble(); out.ituz = qe.value(7).isNull() ? 0 : qe.value(7).toInt(); out.tz = qe.value(8).isNull() ? 0.0f : static_cast(qe.value(8).toDouble()); - out.flag = qe.value(1).toString(); + out.flag = flags.value(qe.value(2).toInt()); applyZoneOverride(out); return out; } } // 3) Longest prefix (join dxcc_entities to get ituz/tz) + DxccEntity best{}; + bool bestFound = false; + { QSqlQuery qp; qp.prepare( @@ -1213,32 +1213,50 @@ DxccEntity Data::lookupCallsign(const QString& callsign, const QDateTime& date) "FROM clublog_prefixes p " "JOIN clublog_entities e ON e.adif = p.adif " "LEFT JOIN dxcc_entities de ON de.id = e.adif " - "WHERE ? LIKE p.call || '%' " - "AND (p.start IS NULL OR p.start <= ?) " - "AND (p.end IS NULL OR p.end >= ?) " - "ORDER BY length(p.call) DESC, p.record DESC " + "WHERE p.call = ? " + "AND (p.start IS '' OR p.start <= ?) " + "AND (p.end IS '' OR p.end >= ?) " + "ORDER BY p.record DESC " "LIMIT 1"); - qp.addBindValue(lookupPrefix); - qp.addBindValue(dateIso); - qp.addBindValue(dateIso); - if (qp.exec() && qp.next()) { - DxccEntity out; - out.country = qp.value(0).toString(); - out.prefix = qp.value(1).toString(); - out.dxcc = qp.value(2).toInt(); - out.cont = qp.value(3).toString(); - out.cqz = qp.value(4).toInt(); - out.latlon[0] = qp.value(5).toDouble(); - out.latlon[1] = qp.value(6).toDouble(); - out.ituz = qp.value(7).isNull() ? 0 : qp.value(7).toInt(); - out.tz = qp.value(8).isNull() ? 0.0f : static_cast(qp.value(8).toDouble()); - out.flag = qp.value(1).toString(); - applyZoneOverride(out); - return out; + // Grow the probe 1 char at a time; keep the last hit as the "longest". + for (int i = 1; i <= lookupPrefix.size(); ++i) { + const QString probe = lookupPrefix.left(i); + + qp.bindValue(0, probe); + qp.bindValue(1, dateIso); + qp.bindValue(2, dateIso); + + if (qp.exec() && qp.next()) { + DxccEntity cand; + cand.country = qp.value(0).toString(); + cand.prefix = qp.value(1).toString(); // the matched prefix (== probe) + cand.dxcc = qp.value(2).toInt(); + cand.cont = qp.value(3).toString(); + cand.cqz = qp.value(4).toInt(); + cand.latlon[0] = qp.value(5).toDouble(); + cand.latlon[1] = qp.value(6).toDouble(); + cand.ituz = qp.value(7).isNull() ? 0 : qp.value(7).toInt(); + cand.tz = qp.value(8).isNull() ? 0.0f : static_cast(qp.value(8).toDouble()); + cand.flag = flags.value(cand.dxcc); + + best = cand; // overwrite with the longer match + bestFound = true; + } else { + // Clear results so we can reuse the prepared statement cleanly + qp.finish(); + } + } } + if (bestFound) { + applyZoneOverride(best); // your existing override hook + return best; + } + + + // 4) No match return e; } From 0651e36a782ca4ed98d09e9e6730b35fe689c96e Mon Sep 17 00:00:00 2001 From: aa5sh <84428382+aa5sh@users.noreply.github.com> Date: Sun, 10 Aug 2025 18:42:07 -0500 Subject: [PATCH 3/8] More GitHub Issues --- core/LOVDownloader.cpp | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/core/LOVDownloader.cpp b/core/LOVDownloader.cpp index 66b4922a..3add33a0 100644 --- a/core/LOVDownloader.cpp +++ b/core/LOVDownloader.cpp @@ -17,6 +17,13 @@ #include "LOVDownloader.h" #include "debug.h" #include +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#include +using XmlTag = QLatin1StringView; // Qt 6 +#else +#include +using XmlTag = QLatin1String; // Qt 5 +#endif MODULE_IDENTIFICATION("qlog.core.lovdownloader"); @@ -1196,7 +1203,7 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre auto readDate = [&](const QString &s)->QString { return s; }; // store ISO8601 text as-is // Cursor down to - while (!xml.atEnd() && !(xml.isStartElement() && xml.name() == "clublog")) xml.readNext(); + while (!xml.atEnd() && !(xml.isStartElement() && xml.name() == XmlTag("clublog"))) xml.readNext(); if (xml.atEnd()) { qWarning() << "ClubLog: not found"; @@ -1213,15 +1220,15 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre if (top == "entities") { // ...... - while (!(xml.isEndElement() && xml.name() == "entities")) { + while (!(xml.isEndElement() && xml.name() == XmlTag("entities"))) { xml.readNext(); - if (xml.isStartElement() && xml.name() == "entity") { + if (xml.isStartElement() && xml.name() == XmlTag("entity")) { // parse one entity int adif = 0; QString name, prefix, cont; bool deleted=false; int cqz = 0; QString start, end, whitelist_start, whitelist_end; bool whitelist=false; double lon=0.0, lat=0.0; - while (!(xml.isEndElement() && xml.name() == "entity")) { + while (!(xml.isEndElement() && xml.name() == XmlTag("entity"))) { xml.readNext(); if (!xml.isStartElement()) continue; const QString tag = xml.name().toString(); @@ -1266,13 +1273,13 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre while (!(xml.isEndElement() && xml.name() == top)) { xml.readNext(); - if (xml.isStartElement() && (xml.name()=="prefix" || xml.name()=="exception")) { + if (xml.isStartElement() && (xml.name()==XmlTag("prefix") || xml.name()==XmlTag("exception"))) { bool ok=false; quint32 rec = xml.attributes().value("record").toUInt(&ok); QString call, entity, cont, start, end; - int adif=0, cqz=0; double lon=NAN, lat=NAN; + int adif=0, cqz=0; double lon=0.0, lat=0.0; - while (!(xml.isEndElement() && (xml.name()=="prefix" || xml.name()=="exception"))) { + while (!(xml.isEndElement() && (xml.name()==XmlTag("prefix") || xml.name()==XmlTag("exception")))) { xml.readNext(); if (!xml.isStartElement()) continue; const QString tag = xml.name().toString(); @@ -1304,12 +1311,12 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre } } else if (top == "invalid_operations") { - while (!(xml.isEndElement() && xml.name()=="invalid_operations")) { + while (!(xml.isEndElement() && xml.name()==XmlTag("invalid_operations"))) { xml.readNext(); - if (xml.isStartElement() && xml.name()=="invalid") { + if (xml.isStartElement() && xml.name()==XmlTag("invalid")) { bool ok=false; quint32 rec = xml.attributes().value("record").toUInt(&ok); QString call, start, end; - while (!(xml.isEndElement() && xml.name()=="invalid")) { + while (!(xml.isEndElement() && xml.name()==XmlTag("invalid"))) { xml.readNext(); if (!xml.isStartElement()) continue; const QString tag = xml.name().toString(); @@ -1327,12 +1334,12 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre } } else if (top == "zone_exceptions") { - while (!(xml.isEndElement() && xml.name()=="zone_exceptions")) { + while (!(xml.isEndElement() && xml.name()==XmlTag("zone_exceptions"))) { xml.readNext(); - if (xml.isStartElement() && xml.name()=="zone_exception") { + if (xml.isStartElement() && xml.name()==XmlTag("zone_exception")) { bool ok=false; quint32 rec = xml.attributes().value("record").toUInt(&ok); QString call, start, end; int zone=0; - while (!(xml.isEndElement() && xml.name()=="zone_exception")) { + while (!(xml.isEndElement() && xml.name()==XmlTag("zone_exception"))) { xml.readNext(); if (!xml.isStartElement()) continue; const QString tag = xml.name().toString(); From 55c97701e03c653606cfdaac3e009e05d340cfc1 Mon Sep 17 00:00:00 2001 From: aa5sh <84428382+aa5sh@users.noreply.github.com> Date: Sun, 10 Aug 2025 18:47:00 -0500 Subject: [PATCH 4/8] Update LOVDownloader.cpp --- core/LOVDownloader.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/LOVDownloader.cpp b/core/LOVDownloader.cpp index 3add33a0..80932e07 100644 --- a/core/LOVDownloader.cpp +++ b/core/LOVDownloader.cpp @@ -18,10 +18,10 @@ #include "debug.h" #include #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -#include +#include using XmlTag = QLatin1StringView; // Qt 6 #else -#include +#include using XmlTag = QLatin1String; // Qt 5 #endif From 4aa775c36feb9f487479c05be71df0a299fd10d7 Mon Sep 17 00:00:00 2001 From: aa5sh <84428382+aa5sh@users.noreply.github.com> Date: Sun, 10 Aug 2025 18:51:34 -0500 Subject: [PATCH 5/8] Another Try I'm not sure what's going on with the Linux builds. trying to find a good fix. Sorry. --- core/LOVDownloader.cpp | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/core/LOVDownloader.cpp b/core/LOVDownloader.cpp index 80932e07..685499be 100644 --- a/core/LOVDownloader.cpp +++ b/core/LOVDownloader.cpp @@ -17,13 +17,6 @@ #include "LOVDownloader.h" #include "debug.h" #include -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) -#include -using XmlTag = QLatin1StringView; // Qt 6 -#else -#include -using XmlTag = QLatin1String; // Qt 5 -#endif MODULE_IDENTIFICATION("qlog.core.lovdownloader"); @@ -1203,7 +1196,7 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre auto readDate = [&](const QString &s)->QString { return s; }; // store ISO8601 text as-is // Cursor down to - while (!xml.atEnd() && !(xml.isStartElement() && xml.name() == XmlTag("clublog"))) xml.readNext(); + while (!xml.atEnd() && !(xml.isStartElement() && xml.name() == QStringLiteral("clublog"))) xml.readNext(); if (xml.atEnd()) { qWarning() << "ClubLog: not found"; @@ -1220,15 +1213,15 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre if (top == "entities") { // ...... - while (!(xml.isEndElement() && xml.name() == XmlTag("entities"))) { + while (!(xml.isEndElement() && xml.name() == QStringLiteral("entities"))) { xml.readNext(); - if (xml.isStartElement() && xml.name() == XmlTag("entity")) { + if (xml.isStartElement() && xml.name() == QStringLiteral("entity")) { // parse one entity int adif = 0; QString name, prefix, cont; bool deleted=false; int cqz = 0; QString start, end, whitelist_start, whitelist_end; bool whitelist=false; double lon=0.0, lat=0.0; - while (!(xml.isEndElement() && xml.name() == XmlTag("entity"))) { + while (!(xml.isEndElement() && xml.name() == QStringLiteral("entity"))) { xml.readNext(); if (!xml.isStartElement()) continue; const QString tag = xml.name().toString(); @@ -1273,13 +1266,13 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre while (!(xml.isEndElement() && xml.name() == top)) { xml.readNext(); - if (xml.isStartElement() && (xml.name()==XmlTag("prefix") || xml.name()==XmlTag("exception"))) { + if (xml.isStartElement() && (xml.name()==QStringLiteral("prefix") || xml.name()==QStringLiteral("exception"))) { bool ok=false; quint32 rec = xml.attributes().value("record").toUInt(&ok); QString call, entity, cont, start, end; int adif=0, cqz=0; double lon=0.0, lat=0.0; - while (!(xml.isEndElement() && (xml.name()==XmlTag("prefix") || xml.name()==XmlTag("exception")))) { + while (!(xml.isEndElement() && (xml.name()==QStringLiteral("prefix") || xml.name()==QStringLiteral("exception")))) { xml.readNext(); if (!xml.isStartElement()) continue; const QString tag = xml.name().toString(); @@ -1311,12 +1304,12 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre } } else if (top == "invalid_operations") { - while (!(xml.isEndElement() && xml.name()==XmlTag("invalid_operations"))) { + while (!(xml.isEndElement() && xml.name()==QStringLiteral("invalid_operations"))) { xml.readNext(); - if (xml.isStartElement() && xml.name()==XmlTag("invalid")) { + if (xml.isStartElement() && xml.name()==QStringLiteral("invalid")) { bool ok=false; quint32 rec = xml.attributes().value("record").toUInt(&ok); QString call, start, end; - while (!(xml.isEndElement() && xml.name()==XmlTag("invalid"))) { + while (!(xml.isEndElement() && xml.name()==QStringLiteral("invalid"))) { xml.readNext(); if (!xml.isStartElement()) continue; const QString tag = xml.name().toString(); @@ -1334,12 +1327,12 @@ void LOVDownloader::parseClubLogCTY(const SourceDefinition &sourceDef, QTextStre } } else if (top == "zone_exceptions") { - while (!(xml.isEndElement() && xml.name()==XmlTag("zone_exceptions"))) { + while (!(xml.isEndElement() && xml.name()==QStringLiteral("zone_exceptions"))) { xml.readNext(); - if (xml.isStartElement() && xml.name()==XmlTag("zone_exception")) { + if (xml.isStartElement() && xml.name()==QStringLiteral("zone_exception")) { bool ok=false; quint32 rec = xml.attributes().value("record").toUInt(&ok); QString call, start, end; int zone=0; - while (!(xml.isEndElement() && xml.name()==XmlTag("zone_exception"))) { + while (!(xml.isEndElement() && xml.name()==QStringLiteral("zone_exception"))) { xml.readNext(); if (!xml.isStartElement()) continue; const QString tag = xml.name().toString(); From 56ca3df93f09cb8a7924184f6c5937eea17fa420 Mon Sep 17 00:00:00 2001 From: Michael Morgan <84428382+aa5sh@users.noreply.github.com> Date: Sun, 31 Aug 2025 22:55:24 -0500 Subject: [PATCH 6/8] Awards and Recompute I modified the DXCC Awards to look at the cty list. Also added an option to recompute the DXCC for log entries. Can filter to just entries with no DXCC or whole log. --- QLog.pro | 2 + data/recomputedxccdialog.cpp | 220 +++++++++++++++++++++++++++++++++++ data/recomputedxccdialog.h | 37 ++++++ ui/AwardsDialog.cpp | 4 +- ui/MainWindow.cpp | 9 ++ ui/MainWindow.h | 1 + ui/MainWindow.ui | 114 ++++++++++-------- 7 files changed, 334 insertions(+), 53 deletions(-) create mode 100644 data/recomputedxccdialog.cpp create mode 100644 data/recomputedxccdialog.h diff --git a/QLog.pro b/QLog.pro index 9d2186eb..dc8448e8 100644 --- a/QLog.pro +++ b/QLog.pro @@ -88,6 +88,7 @@ SOURCES += \ data/SerialPort.cpp \ data/StationProfile.cpp \ data/UpdatableSQLRecord.cpp \ + data/recomputedxccdialog.cpp \ logformat/AdiFormat.cpp \ logformat/AdxFormat.cpp \ logformat/CSVFormat.cpp \ @@ -229,6 +230,7 @@ HEADERS += \ data/WWFFEntity.h \ data/WWVSpot.h \ data/WsjtxEntry.h \ + data/recomputedxccdialog.h \ logformat/AdiFormat.h \ logformat/AdxFormat.h \ logformat/CSVFormat.h \ diff --git a/data/recomputedxccdialog.cpp b/data/recomputedxccdialog.cpp new file mode 100644 index 00000000..37333fb3 --- /dev/null +++ b/data/recomputedxccdialog.cpp @@ -0,0 +1,220 @@ +// RecomputeDxccDialog.cpp +#include "RecomputeDxccDialog.h" +#include "Data.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "core/debug.h" + +MODULE_IDENTIFICATION("qlog.data.antprofile"); + +static QDateTime parseIsoDateTimeUtc(const QString &s) +{ + FCT_IDENTIFICATION; + +#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0) + QDateTime dt = QDateTime::fromString(s, Qt::ISODateWithMs); + if (!dt.isValid()) + dt = QDateTime::fromString(s, Qt::ISODate); +#else + QDateTime dt = QDateTime::fromString(s, Qt::ISODate); +#endif + return dt.toUTC(); +} + +RecomputeDxccDialog::RecomputeDxccDialog(QWidget *parent) + : QDialog(parent) +{ + FCT_IDENTIFICATION; + + setWindowTitle("Recompute DXCC"); + resize(720, 420); + + auto *v = new QVBoxLayout(this); + + onlyMissingCheck_ = new QCheckBox(tr("Only records with empty DXCC"), this); + onlyMissingCheck_->setToolTip(tr("If checked, only contacts where DXCC is NULL or 0 will be processed.")); + onlyMissingCheck_->setChecked(false); // default off (process all) + v->addWidget(onlyMissingCheck_); + + m_bar = new QProgressBar(this); + m_bar->setRange(0, 0); + v->addWidget(m_bar); + + m_stats = new QLabel("Processed: 0 | Updated: 0", this); + v->addWidget(m_stats); + + m_log = new QTextEdit(this); + m_log->setReadOnly(true); + v->addWidget(m_log, 1); + + auto* btns = new QHBoxLayout(); + m_startBtn = new QPushButton(tr("Start"), this); + m_cancelBtn = new QPushButton(tr("Close"), this); + btns->addStretch(); + btns->addWidget(m_startBtn); + btns->addWidget(m_cancelBtn); + + connect(m_startBtn, &QPushButton::clicked, this, &RecomputeDxccDialog::onStart); + connect(m_cancelBtn, &QPushButton::clicked, this, &RecomputeDxccDialog::onCancel); + + v->addLayout(btns); + + // Fire the job in a thread so UI stays alive + // QTimer::singleShot(0, this, &RecomputeDxccDialog::runJob); +} + +RecomputeDxccDialog::~RecomputeDxccDialog() +{ + FCT_IDENTIFICATION; + + m_cancel.store(true); +} + +void RecomputeDxccDialog::onCancel() +{ + FCT_IDENTIFICATION; + + m_cancel.store(true); + m_cancelBtn->setEnabled(false); + accept(); +} + +void RecomputeDxccDialog::onStart() +{ + FCT_IDENTIFICATION; + + m_startBtn->setEnabled(false); + onlyMissingCheck_->setEnabled(false); + m_log->clear(); + + QTimer::singleShot(0, this, &RecomputeDxccDialog::runJob); +} + +void RecomputeDxccDialog::logLine(const QString &line) +{ + FCT_IDENTIFICATION; + + m_log->append(line); +} + +void RecomputeDxccDialog::runJob() +{ + FCT_IDENTIFICATION; + + stopRequested = false; + m_log->clear(); + + // nice header in the log + m_log->append(QStringLiteral("=== Recompute DXCC started @ %1 ===") + .arg(QDateTime::currentDateTime().toString(Qt::ISODate))); + + QSqlQuery countQuery; + QString countSql = "SELECT COUNT(*) FROM contacts"; + if (onlyMissingCheck_->isChecked()) { + countSql += " WHERE dxcc = 0"; // per your request + } + if (!countQuery.exec(countSql) || !countQuery.next()) { + m_log->append("Failed to count contacts: " + countQuery.lastError().text()); + m_stats->setText("Failed."); + return; + } + int total = countQuery.value(0).toInt(); + if (total == 0) { + m_stats->setText("No contacts found to process."); + m_log->append("No contacts match the current filter. Nothing to do."); + return; + } + + m_bar->setRange(0, total); + m_bar->setValue(0); + + QString sql = "SELECT id, callsign, start_time, dxcc, country FROM contacts"; + if (onlyMissingCheck_->isChecked()) { + sql += " WHERE dxcc = 0 or dxcc is null "; // per your request + } + sql += " ORDER BY id"; // keeps UI updates smoother/predictable + + QSqlQuery q; + if (!q.exec(sql)) { + m_log->append("Failed to query contacts: " + q.lastError().text()); + m_stats->setText("Failed."); + return; + } + + int processed = 0, updated = 0; + + while (q.next() && !stopRequested) { + const int id = q.value(0).toInt(); + const QString callsign = q.value(1).toString(); + const QString startIso = q.value(2).toString(); + const int oldDxcc = q.value(3).toInt(); + const QString oldCtry = q.value(4).toString(); + + // robust ISO parsing + normalize to UTC + QDateTime dt = QDateTime::fromString(startIso, Qt::ISODateWithMs); + if (!dt.isValid()) dt = QDateTime::fromString(startIso, Qt::ISODate); + if (!dt.isValid()) dt = QDateTime::fromString(startIso, "yyyy-MM-dd'T'HH:mm:sszzz"); + if (!dt.isValid()) dt = QDateTime::currentDateTimeUtc(); + dt = dt.toUTC(); + + const DxccEntity ent = Data::instance()->lookupCallsign(callsign, dt); + + // update only if something actually changed and we have a meaningful dxcc + const bool changed = (ent.dxcc != 0) && (ent.dxcc != oldDxcc); + if (changed) { + QSqlQuery u; + u.prepare("UPDATE contacts SET dxcc = ?, country = ?, cont = ?, cqz = ?, ituz = ?, pfx = ? WHERE id = ?"); + u.addBindValue(ent.dxcc); + u.addBindValue(ent.country); + u.addBindValue(ent.cont); + u.addBindValue(ent.cqz); + u.addBindValue(ent.ituz); + u.addBindValue(ent.prefix); + u.addBindValue(id); + + if (!u.exec()) { + m_log->append(QString("Failed to update %1 (id=%2): %3") + .arg(callsign).arg(id).arg(u.lastError().text())); + } else { + ++updated; + m_log->append(QString("Updated %1 (id=%2) @ %3 %4 -> %5 (DXCC %6)") + .arg(callsign) + .arg(id) + .arg(dt.toString(Qt::ISODate)) + .arg(oldCtry.isEmpty() ? QStringLiteral("") : oldCtry) + .arg(ent.country) + .arg(ent.dxcc)); + } + } + + ++processed; + m_bar->setValue(processed); + m_stats->setText(QString("Processed %1/%2, Updated %3") + .arg(processed).arg(total).arg(updated)); + QCoreApplication::processEvents(); + } + + // final status + m_stats->setText(QString("Done. Processed %1, Updated %2").arg(processed).arg(updated)); + + // append a summary block to the log (no popup) + m_log->append(QString()); + m_log->append(QStringLiteral("=== Summary ===")); + m_log->append(QString("Processed: %1").arg(processed)); + m_log->append(QString("Updated: %1").arg(updated)); + m_log->append(QStringLiteral("Filter: %1") + .arg(onlyMissingCheck_->isChecked() + ? QStringLiteral("Only records with DXCC = 0") + : QStringLiteral("All records"))); + m_log->append(QStringLiteral("Finished @ %1") + .arg(QDateTime::currentDateTime().toString(Qt::ISODate))); +} diff --git a/data/recomputedxccdialog.h b/data/recomputedxccdialog.h new file mode 100644 index 00000000..8a802c86 --- /dev/null +++ b/data/recomputedxccdialog.h @@ -0,0 +1,37 @@ +// RecomputeDxccDialog.h +#pragma once +#include +#include +#include +#include + +class QProgressBar; +class QLabel; +class QTextEdit; +class QPushButton; +class QThread; + +class RecomputeDxccDialog : public QDialog +{ + Q_OBJECT +public: + explicit RecomputeDxccDialog(QWidget *parent = nullptr); + ~RecomputeDxccDialog(); + +private slots: + void onCancel(); + void onStart(); + void runJob(); + +private: + void logLine(const QString &line); + + QProgressBar *m_bar; + QLabel *m_stats; + QTextEdit *m_log; + QPushButton *m_cancelBtn; + QPushButton *m_startBtn; + QCheckBox* onlyMissingCheck_ = nullptr; + bool stopRequested; + std::atomic m_cancel{false}; +}; diff --git a/ui/AwardsDialog.cpp b/ui/AwardsDialog.cpp index 2ca00bb4..b565638c 100644 --- a/ui/AwardsDialog.cpp +++ b/ui/AwardsDialog.cpp @@ -150,8 +150,8 @@ void AwardsDialog::refreshTable(int) setNotWorkedEnabled(true); const QString &entitySelected = getSelectedEntity(); headersColumns = "translate_to_locale(d.name) col1, d.prefix col2 "; - sqlPartDetailTable = " FROM (SELECT id, name, prefix FROM dxcc_entities " - " UNION SELECT DISTINCT dxcc, dxcc, '" + tr("Unknown") + "' as prefix FROM source_contacts a LEFT JOIN dxcc_entities b ON a.dxcc = b.id WHERE b.id IS NULL) d " + sqlPartDetailTable = " FROM (SELECT adif as id, name, prefix FROM clublog_entities " + " UNION SELECT DISTINCT dxcc, dxcc, '" + tr("Unknown") + "' as prefix FROM source_contacts a LEFT JOIN clublog_entities b ON a.dxcc = b.adif WHERE b.adif IS NULL) d " " LEFT OUTER JOIN source_contacts c ON d.id = c.dxcc" " LEFT OUTER JOIN modes m on c.mode = m.name" " WHERE (c.id is NULL or c.my_dxcc = '" + entitySelected + "') "; diff --git a/ui/MainWindow.cpp b/ui/MainWindow.cpp index 57ba9c4b..93217797 100644 --- a/ui/MainWindow.cpp +++ b/ui/MainWindow.cpp @@ -30,6 +30,7 @@ #include "ui/DownloadQSLDialog.h" #include "ui/UploadQSODialog.h" #include "core/LogParam.h" +#include "data/RecomputeDxccDialog.h" MODULE_IDENTIFICATION("qlog.ui.mainwindow"); @@ -1549,6 +1550,14 @@ void MainWindow::showAwards() dialog.exec(); } +void MainWindow::recomputeDxcc() +{ + FCT_IDENTIFICATION; + + RecomputeDxccDialog dlg(this); + dlg.exec(); +} + void MainWindow::showAbout() { FCT_IDENTIFICATION; diff --git a/ui/MainWindow.h b/ui/MainWindow.h index 63c5819e..0ce31781 100644 --- a/ui/MainWindow.h +++ b/ui/MainWindow.h @@ -55,6 +55,7 @@ private slots: void importLog(); void exportLog(); void showAwards(); + void recomputeDxcc(); void showAbout(); void showWikiHelp(); void showMailingList(); diff --git a/ui/MainWindow.ui b/ui/MainWindow.ui index 4d0f129e..4aede16f 100644 --- a/ui/MainWindow.ui +++ b/ui/MainWindow.ui @@ -7,14 +7,14 @@ 0 0 913 - 558 + 580 - QMainWindow::AllowNestedDocks|QMainWindow::AllowTabbedDocks|QMainWindow::AnimatedDocks|QMainWindow::GroupedDragging + QMainWindow::DockOption::AllowNestedDocks|QMainWindow::DockOption::AllowTabbedDocks|QMainWindow::DockOption::AnimatedDocks|QMainWindow::DockOption::GroupedDragging @@ -40,7 +40,7 @@ - Qt::ClickFocus + Qt::FocusPolicy::ClickFocus @@ -52,7 +52,7 @@ 0 0 913 - 23 + 42 @@ -75,6 +75,7 @@ + @@ -173,7 +174,7 @@ false - Qt::ToolButtonIconOnly + Qt::ToolButtonStyle::ToolButtonIconOnly false @@ -211,7 +212,7 @@ - Qt::ClickFocus + Qt::FocusPolicy::ClickFocus @@ -307,8 +308,7 @@ - - .. + Quit @@ -320,7 +320,7 @@ Ctrl+Q - QAction::QuitRole + QAction::MenuRole::QuitRole true @@ -328,20 +328,18 @@ - - .. + &Settings - QAction::PreferencesRole + QAction::MenuRole::PreferencesRole - - .. + New QSO - Clear @@ -358,8 +356,7 @@ - - .. + &Import @@ -367,8 +364,7 @@ - - .. + &Export @@ -387,20 +383,18 @@ - - .. + &About - QAction::AboutRole + QAction::MenuRole::AboutRole - - .. + New QSO - Save @@ -435,8 +429,7 @@ - - .. + S&tatistics @@ -491,8 +484,7 @@ - - .. + &Awards @@ -526,8 +518,7 @@ - - .. + &Wiki @@ -599,10 +590,10 @@ Ctrl+F - Qt::ApplicationShortcut + Qt::ShortcutContext::ApplicationShortcut - QAction::NoRole + QAction::MenuRole::NoRole true @@ -619,10 +610,10 @@ Ctrl+M - Qt::ApplicationShortcut + Qt::ShortcutContext::ApplicationShortcut - QAction::NoRole + QAction::MenuRole::NoRole true @@ -636,10 +627,10 @@ Ctrl+PgDown - Qt::ApplicationShortcut + Qt::ShortcutContext::ApplicationShortcut - QAction::NoRole + QAction::MenuRole::NoRole true @@ -653,10 +644,10 @@ Ctrl+PgUp - Qt::ApplicationShortcut + Qt::ShortcutContext::ApplicationShortcut - QAction::NoRole + QAction::MenuRole::NoRole true @@ -670,10 +661,10 @@ Alt+Return - Qt::ApplicationShortcut + Qt::ShortcutContext::ApplicationShortcut - QAction::NoRole + QAction::MenuRole::NoRole true @@ -687,10 +678,10 @@ Alt+Up - Qt::ApplicationShortcut + Qt::ShortcutContext::ApplicationShortcut - QAction::NoRole + QAction::MenuRole::NoRole true @@ -704,10 +695,10 @@ Alt+Down - Qt::ApplicationShortcut + Qt::ShortcutContext::ApplicationShortcut - QAction::NoRole + QAction::MenuRole::NoRole true @@ -721,10 +712,10 @@ Alt+Right - Qt::ApplicationShortcut + Qt::ShortcutContext::ApplicationShortcut - QAction::NoRole + QAction::MenuRole::NoRole true @@ -738,10 +729,10 @@ Alt+Left - Qt::ApplicationShortcut + Qt::ShortcutContext::ApplicationShortcut - QAction::NoRole + QAction::MenuRole::NoRole true @@ -755,13 +746,13 @@ Alt+\ - Qt::ApplicationShortcut + Qt::ShortcutContext::ApplicationShortcut false - QAction::NoRole + QAction::MenuRole::NoRole true @@ -853,8 +844,7 @@ - - .. + Upload @@ -883,6 +873,11 @@ true + + + Recompute DXCC + + @@ -1614,6 +1609,22 @@ + + actionRecompute_DXCC + triggered() + MainWindow + recomputeDxcc() + + + -1 + -1 + + + 456 + 289 + + + settingsChanged() @@ -1628,6 +1639,7 @@ rotConnect() QSOFilterSetting() showAwards() + recomputeDxcc() alertRuleSetting() showAlerts() clearAlerts() From 46163bfc423663e86a4b514db86cc632f1c0c3a8 Mon Sep 17 00:00:00 2001 From: Michael Morgan <84428382+aa5sh@users.noreply.github.com> Date: Sun, 31 Aug 2025 22:59:07 -0500 Subject: [PATCH 7/8] Update recomputedxccdialog.cpp fix path --- data/recomputedxccdialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/recomputedxccdialog.cpp b/data/recomputedxccdialog.cpp index 37333fb3..1ed3c3d3 100644 --- a/data/recomputedxccdialog.cpp +++ b/data/recomputedxccdialog.cpp @@ -1,5 +1,5 @@ // RecomputeDxccDialog.cpp -#include "RecomputeDxccDialog.h" +#include "data/recomputedxccdialog.h" #include "Data.h" #include #include From d3d5af38186b8d510b5856098e83b774153dcdbe Mon Sep 17 00:00:00 2001 From: Michael Morgan <84428382+aa5sh@users.noreply.github.com> Date: Sun, 31 Aug 2025 23:03:57 -0500 Subject: [PATCH 8/8] Update MainWindow.cpp --- ui/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/MainWindow.cpp b/ui/MainWindow.cpp index 93217797..43a62c25 100644 --- a/ui/MainWindow.cpp +++ b/ui/MainWindow.cpp @@ -30,7 +30,7 @@ #include "ui/DownloadQSLDialog.h" #include "ui/UploadQSODialog.h" #include "core/LogParam.h" -#include "data/RecomputeDxccDialog.h" +#include "data/recomputedxccdialog.h" MODULE_IDENTIFICATION("qlog.ui.mainwindow");