Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ compile_commands.json
# QtCreator local machine specific files for imported projects
*creator.user*
.DS_Store
/build
8 changes: 8 additions & 0 deletions logformat/AdiFormat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,14 @@ void AdiFormat::contactFields2SQLRecord(QMap<QString, QVariant> &contact, QSqlRe
time_on = time_off;
}

/* Records that have a date but no time (e.g. LoTW DXCC credit exports) would
* produce an invalid QDateTime, making them impossible to match later.
* Default to midnight UTC so the record remains usable. */
if ( !time_on.isValid() && date_on.isValid() )
{
time_on = QTime(0, 0, 0);
}

QDateTime start_time(date_on, time_on, QTimeZone::utc());
QDateTime end_time(date_off, time_off, QTimeZone::utc());

Expand Down
130 changes: 130 additions & 0 deletions logformat/LogFormat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,136 @@ unsigned long LogFormat::runImport(QTextStream& importLogStream,

#undef RECORDIDX

void LogFormat::runQSOCreditImport(QSLFrom /*fromService*/)
{
FCT_IDENTIFICATION;

auto reportFormatter = [&](const QDateTime &qsoTime,
const QString &callsign,
const QString &mode,
const QStringList addInfo = QStringList())
{
return QString("%0; %1; %2%3 %4").arg(qsoTime.isValid() ? qsoTime.toString(locale.formatDateShortWithYYYY()) : "-",
callsign,
mode,
(addInfo.size() > 0 ) ? ";" : "",
addInfo.join(", "));
};

QSLMergeStat stats = {QStringList(), QStringList(), QStringList(), QStringList(), 0};
this->importStart();

QSqlTableModel model;
model.setTable("contacts");
QSqlRecord QSLRecord = model.record(0);

while ( true )
{
QSLRecord.clearValues();

if ( !this->importNext(QSLRecord) ) break;

stats.qsosDownloaded++;

if ( stats.qsosDownloaded % 100 == 0 )
{
emit importPosition(stream.pos());
}

const QVariant &call = QSLRecord.value("callsign");
const QVariant &band = QSLRecord.value("band");
const QVariant &start_time = QSLRecord.value("start_time");
const QVariant &satName = QSLRecord.value("sat_name");

/* require at minimum: callsign, band, and a valid date */
if ( !start_time.toDateTime().isValid()
|| call.toString().isEmpty()
|| band.toString().isEmpty() )
{
qWarning() << "DXCC credit import: missing start_time, callsign, or band";
qCDebug(runtime) << QSLRecord;
stats.errorQSLs.append(reportFormatter(start_time.toDateTime(), call.toString(), ""));
continue;
}

/* Match on callsign + band + date + must already have a QSL confirmed.
* Mode is intentionally omitted because DXCC credit records may use generic
* mode group names that do not precisely match the logged mode.
* A ±1 day tolerance is applied because the credit file may record the QSO
* date in the operator's local time zone while the DB stores UTC, causing a
* one-day discrepancy for QSOs made near midnight. */
const QString matchFilter = QString(
"callsign=upper('%1') AND upper(band)=upper('%2') AND "
"COALESCE(sat_name, '') = upper('%3') AND "
"ABS(JULIANDAY(date(start_time)) - JULIANDAY(date('%4'))) <= 1 AND "
"(qsl_rcvd = 'Y' OR lotw_qsl_rcvd = 'Y')"
).arg(call.toString(),
band.toString(),
satName.toString(),
start_time.toDateTime().toTimeZone(QTimeZone::utc()).toString("yyyy-MM-dd hh:mm:ss"));

model.setFilter(matchFilter);
model.select();

if ( model.rowCount() < 1 )
{
stats.unmatchedQSLs.append(reportFormatter(start_time.toDateTime(), call.toString(), ""));
continue;
}

qCDebug(runtime) << "DXCC credit: found" << model.rowCount() << "match(es) for"
<< call.toString() << band.toString() << start_time.toString();

/* Update every matching contact — multiple QSOs on the same date/band are possible. */
for ( int row = 0; row < model.rowCount(); ++row )
{
QSqlRecord originalRecord = model.record(row);

QStringList updatedFields;
bool callUpdate = false;

auto conditionUpdate = [&](const QString &contactKey,
const QString &qslKey)
{
if ( !QSLRecord.value(qslKey).toString().isEmpty()
&& originalRecord.value(contactKey).toString().isEmpty() )
{
qCDebug(runtime) << "Updating:" << contactKey
<< "to" << QSLRecord.value(qslKey).toString();
updatedFields.append(contactKey + "(" + QSLRecord.value(qslKey).toString() + ")");
originalRecord.setValue(contactKey, QSLRecord.value(qslKey));
return true;
}
return false;
};

callUpdate |= conditionUpdate("credit_granted", "credit_granted");
callUpdate |= conditionUpdate("credit_submitted", "credit_submitted");

if ( callUpdate )
{
if ( !model.setRecord(row, originalRecord) )
{
qWarning() << "Cannot update a Contact record - " << model.lastError();
qCDebug(runtime) << originalRecord;
}
stats.updatedQSOs.append(reportFormatter(start_time.toDateTime(), call.toString(), "", updatedFields));
}
}

if ( !model.submitAll() )
{
qWarning() << "Cannot commit changes to Contact Table - " << model.lastError();
}
}

emit importPosition(stream.pos());

this->importEnd();

emit QSLMergeFinished(stats);
}

void LogFormat::runQSLImport(QSLFrom fromService)
{
FCT_IDENTIFICATION;
Expand Down
2 changes: 2 additions & 0 deletions logformat/LogFormat.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class LogFormat : public QObject {
enum QSLFrom {
LOTW,
EQSL,
LOTW_DXCC,
UNKNOW
};

Expand All @@ -57,6 +58,7 @@ class LogFormat : public QObject {
unsigned long *warnings,
unsigned long *errors);
void runQSLImport(QSLFrom fromService);
void runQSOCreditImport(QSLFrom fromService);
long runExport();
long runExport(const QList<QSqlRecord>&);
void setDefaults(QMap<QString, QString>& defaults);
Expand Down
35 changes: 23 additions & 12 deletions service/lotw/Lotw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -434,21 +434,29 @@ void LotwQSLDownloader::receiveQSL(const QDate &start_date, bool qso_since, cons
qCDebug(function_parameters) << start_date << " " << qso_since;

QList<QPair<QString, QString>> params;
params.append(qMakePair(QString("qso_query"), QString("1")));
params.append(qMakePair(QString("qso_qsldetail"), QString("yes")));
params.append(qMakePair(QString("qso_owncall"), station_callsign));

const QString &start = start_date.toString("yyyy-MM-dd");

if (qso_since)
if ( LotwDXCCCredits )
{
params.append(qMakePair(QString("qso_qsl"), QString("no")));
params.append(qMakePair(QString("qso_qsorxsince"), start));
params.append(qMakePair(QString("ac_acct"), QString("1")));
}
else
{
params.append(qMakePair(QString("qso_qsl"), QString("yes")));
params.append(qMakePair(QString("qso_qslsince"), start));
params.append(qMakePair(QString("qso_query"), QString("1")));
params.append(qMakePair(QString("qso_qsldetail"), QString("yes")));
params.append(qMakePair(QString("qso_owncall"), station_callsign));

const QString &start = start_date.toString("yyyy-MM-dd");

if (qso_since)
{
params.append(qMakePair(QString("qso_qsl"), QString("no")));
params.append(qMakePair(QString("qso_qsorxsince"), start));
}
else
{
params.append(qMakePair(QString("qso_qsl"), QString("yes")));
params.append(qMakePair(QString("qso_qslsince"), start));
}
}

get(params);
Expand Down Expand Up @@ -540,7 +548,10 @@ void LotwQSLDownloader::processReply(QNetworkReply *reply)
emit receiveQSLComplete(stats);
});

adi.runQSLImport(adi.LOTW);
if ( LotwDXCCCredits )
adi.runQSOCreditImport(adi.LOTW_DXCC);
else
adi.runQSLImport(adi.LOTW);

tempFile.close();

Expand All @@ -559,7 +570,7 @@ void LotwQSLDownloader::get(QList<QPair<QString, QString>> params)
query.addQueryItem("login", username.toUtf8().toPercentEncoding());
query.addQueryItem("password", password.toUtf8().toPercentEncoding());

QUrl url(ADIF_API);
QUrl url(LotwDXCCCredits ? DXCC_CREDIT_API : ADIF_API);
url.setQuery(query);

qCDebug(runtime) << Data::safeQueryString(query);
Expand Down
3 changes: 3 additions & 0 deletions service/lotw/Lotw.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ class LotwQSLDownloader : public GenericQSLDownloader, private LotwBase
explicit LotwQSLDownloader(QObject *parent = nullptr);
virtual ~LotwQSLDownloader();

bool LotwDXCCCredits = false;

virtual void receiveQSL(const QDate &, bool, const QString &) override;

public slots:
Expand All @@ -103,6 +105,7 @@ public slots:
private:
QNetworkReply *currentReply;
const QString ADIF_API = "https://lotw.arrl.org/lotwuser/lotwreport.adi";
const QString DXCC_CREDIT_API = "https://lotw.arrl.org/lotwuser/logbook/qslcards.php";

virtual void processReply(QNetworkReply* reply) override;
void get(QList<QPair<QString, QString>> params);
Expand Down
16 changes: 16 additions & 0 deletions ui/DownloadQSLDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ DownloadQSLDialog::DownloadQSLDialog(QWidget *parent)
"FROM contacts ORDER BY station_callsign", "", ui->lotwMyCallsignCombo));
ui->lotwDateEdit->setDisplayFormat(locale.formatDateShortWithYYYY());
ui->eqslDateEdit->setDisplayFormat(locale.formatDateShortWithYYYY());

ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("&Download"));

const StationProfile &profile = StationProfilesManager::instance()->getCurProfile1();
Expand All @@ -44,6 +45,9 @@ DownloadQSLDialog::DownloadQSLDialog(QWidget *parent)
ui->lotwGroupBox->setChecked(false);
ui->lotwGroupBox->setEnabled(false);
ui->lotwGroupBox->setToolTip(tr("LoTW is not configured properly.<p> Please, use <b>Settings</b> dialog to configure it.</p>"));
ui->lotwDXCCCheckBox->setChecked(false);
ui->lotwDXCCCheckBox->setEnabled(false);
ui->lotwDXCCCheckBox->setToolTip(tr("LoTW is not configured properly.<p> Please, use <b>Settings</b> dialog to configure it.</p>"));
}

if ( EQSLBase::getUsername().isEmpty() )
Expand Down Expand Up @@ -97,6 +101,9 @@ void DownloadQSLDialog::loadDialogState()
ui->eqslDateTypeCombo->setCurrentIndex((LogParam::getDownloadQSLServiceLastQSOQSL("eqsl")) ? 0 : 1);

ui->eqslQTHProfileEdit->setText(LogParam::getDownloadQSLeQSLLastProfile());

/* LoTW DXCC Credits checkbox always starts unchecked — this is a one-off task. */
ui->lotwDXCCCheckBox->setChecked(false);
}

void DownloadQSLDialog::saveDialogState()
Expand Down Expand Up @@ -205,6 +212,15 @@ void DownloadQSLDialog::downloadQSLs()
lotw->receiveQSL(ui->lotwDateEdit->date(), !qslSinceActive, ui->lotwMyCallsignCombo->currentText());
});

if ( ui->lotwDXCCCheckBox->isChecked() )
downloadQueue.enqueue([=]()
{
LotwQSLDownloader* lotw = new LotwQSLDownloader(this);
lotw->LotwDXCCCredits = true;
prepareDownload(lotw, tr("LoTW DXCC Credits"), false, "lotwdxcc");
lotw->receiveQSL(QDate(), false, QString());
});

if ( downloadQueue.isEmpty() )
{
QMessageBox::information(this, tr("QLog Information"), tr("No service selected"));
Expand Down
12 changes: 10 additions & 2 deletions ui/DownloadQSLDialog.ui
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>403</width>
<height>270</height>
<height>290</height>
</rect>
</property>
<property name="windowTitle">
Expand Down Expand Up @@ -109,7 +109,14 @@
</layout>
</widget>
</item>
<item row="2" column="1">
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="lotwDXCCCheckBox">
<property name="text">
<string>LoTW DXCC Credits</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
Expand All @@ -130,6 +137,7 @@
<tabstop>lotwDateTypeCombo</tabstop>
<tabstop>lotwDateEdit</tabstop>
<tabstop>lotwMyCallsignCombo</tabstop>
<tabstop>lotwDXCCCheckBox</tabstop>
</tabstops>
<resources/>
<connections>
Expand Down