From b12d202735fd5e5a99f75ef98a9b9750ccf27e03 Mon Sep 17 00:00:00 2001 From: Kyle Boyle Date: Sat, 23 Nov 2024 19:18:53 -0400 Subject: [PATCH] adds pota export type Park activations are uploaded to pota.app with a specific set of requirements. This change allows a user to directly export&generate the files to import into pota.app without having to use a 3rd party tool to do transformations or manually fiddle with data. --- QLog.pro | 2 + logformat/AdiFormat.cpp | 1 - logformat/LogFormat.cpp | 7 ++ logformat/LogFormat.h | 3 +- logformat/PotaAdiFormat.cpp | 139 ++++++++++++++++++++++++++++++++++++ logformat/PotaAdiFormat.h | 37 ++++++++++ ui/ExportDialog.cpp | 32 +++++++-- ui/ExportDialog.h | 19 ++++- ui/ExportDialog.ui | 36 ++++++++-- 9 files changed, 259 insertions(+), 17 deletions(-) create mode 100644 logformat/PotaAdiFormat.cpp create mode 100644 logformat/PotaAdiFormat.h diff --git a/QLog.pro b/QLog.pro index 3264bb6b..8d0ce219 100644 --- a/QLog.pro +++ b/QLog.pro @@ -102,6 +102,7 @@ SOURCES += \ logformat/CSVFormat.cpp \ logformat/JsonFormat.cpp \ logformat/LogFormat.cpp \ + logformat/PotaAdiFormat.cpp \ models/AlertTableModel.cpp \ models/AwardsTableModel.cpp \ models/DxccTableModel.cpp \ @@ -241,6 +242,7 @@ HEADERS += \ logformat/CSVFormat.h \ logformat/JsonFormat.h \ logformat/LogFormat.h \ + logformat/PotaAdiFormat.h \ models/AlertTableModel.h \ models/AwardsTableModel.h \ models/DxccTableModel.h \ diff --git a/logformat/AdiFormat.cpp b/logformat/AdiFormat.cpp index d5b4c1d6..779f1ff3 100644 --- a/logformat/AdiFormat.cpp +++ b/logformat/AdiFormat.cpp @@ -13,7 +13,6 @@ void AdiFormat::exportStart() { FCT_IDENTIFICATION; - stream << "### QLog ADIF Export\n"; writeField("ADIF_VER", ALWAYS_PRESENT, ADIF_VERSION_STRING); writeField("PROGRAMID", ALWAYS_PRESENT, PROGRAMID_STRING); writeField("PROGRAMVERSION", ALWAYS_PRESENT, VERSION); diff --git a/logformat/LogFormat.cpp b/logformat/LogFormat.cpp index 4bdc9c2b..45a8b7b2 100644 --- a/logformat/LogFormat.cpp +++ b/logformat/LogFormat.cpp @@ -3,6 +3,7 @@ #include "LogFormat.h" #include "AdiFormat.h" #include "AdxFormat.h" +#include "PotaAdiFormat.h" #include "JsonFormat.h" #include "CSVFormat.h" #include "data/Data.h" @@ -51,6 +52,9 @@ LogFormat* LogFormat::open(QString type, QTextStream& stream) { else if (type == "cabrillo") { return open(LogFormat::JSON, stream); } + else if (type == "pota") { + return open(LogFormat::POTA, stream); + } else { return nullptr; } @@ -77,6 +81,9 @@ LogFormat* LogFormat::open(LogFormat::Type type, QTextStream& stream) { case LogFormat::CABRILLO: return nullptr; + case LogFormat::POTA: + return new PotaAdiFormat(stream); + default: return nullptr; } diff --git a/logformat/LogFormat.h b/logformat/LogFormat.h index 64fb2d5c..a6e29e38 100644 --- a/logformat/LogFormat.h +++ b/logformat/LogFormat.h @@ -28,7 +28,8 @@ class LogFormat : public QObject { ADX, CABRILLO, JSON, - CSV + CSV, + POTA }; enum QSLFrom { diff --git a/logformat/PotaAdiFormat.cpp b/logformat/PotaAdiFormat.cpp new file mode 100644 index 00000000..e430ad0e --- /dev/null +++ b/logformat/PotaAdiFormat.cpp @@ -0,0 +1,139 @@ +#include "PotaAdiFormat.h" +#include +#include +#include +#include "core/debug.h" +#include + +MODULE_IDENTIFICATION("qlog.logformat.potalogformat"); + +#define ALWAYS_PRESENT true + +PotaAdiFormat::PotaAdiFormat(QTextStream &stream) + : AdiFormat(stream) + , currentDate(QDateTime::currentDateTime()) +{ + FCT_IDENTIFICATION; +} + +void PotaAdiFormat::setExportInfo(QFile &exportFile) +{ + this->exportInfo = new QFileInfo(exportFile); +} + +void PotaAdiFormat::exportContact(const QSqlRecord &sourceRecord, QMap *applTags) +{ + FCT_IDENTIFICATION; + if (this->exportInfo == nullptr) { + return; + } + // break single record into child activated park records + // assign records to files + QList records = PotaAdiFormat::splitActivatedParks(sourceRecord); + for (QSqlRecord &record : records) { + duplicateField(record, "my_pota_ref", "my_sig"); + record.field("my_sig").setValue(QString("POTA")); + duplicateField(record, "my_pota_ref", "my_sig_info"); + if (!record.field("pota_ref").isNull()) { + duplicateField(record, "pota_ref", "sig_info"); + duplicateField(record, "pota_ref", "sig"); + record.field("sig").setValue(QString("POTA")); + } + AdiFormat *parkOut = this->getParkFile(record); + parkOut->exportContact(record, applTags); + // let parent do ADI export as normal to specified file + AdiFormat::exportContact(record, applTags); + } +} + +AdiFormat *PotaAdiFormat::getParkFile(const QSqlRecord &record) +{ + // https://docs.pota.app/docs/activator_reference/logging_made_easy.html#naming-your-files + // station_callsign@park#-yyyymmdd + QString parkFileName(record.field("station_callsign").value().toString() + "@" + + record.field("my_sig_info").value().toString() + "-" + + currentDate.toString("yyyyMMdd-hhmm") + ".adif"); + + if (!parkFormats.contains(parkFileName)) { + parkFiles[parkFileName] = new QFile(exportInfo->canonicalPath() + QDir::separator() + + parkFileName); + if (!parkFiles[parkFileName]->open(QFile::WriteOnly | QFile::Text)) { + qCCritical(runtime) << "Could not open POTA park file for writing " + << parkFiles[parkFileName]->fileName(); + } + QTextStream *parkStream = new QTextStream(parkFiles[parkFileName]); + parkFormats[parkFileName] = new AdiFormat(*parkStream); + parkFormats[parkFileName]->exportStart(); + } + qCDebug(runtime) << "using park file " << parkFileName << " is open? " + << parkFiles[parkFileName]->isOpen(); + + return parkFormats[parkFileName]; +} + +QList PotaAdiFormat::splitActivatedParks(const QSqlRecord &record) +{ + FCT_IDENTIFICATION; + // can contain multiple parks as a csv: + // K-0817,K-4566,K-4576,K-4573,K-4578@US-WY + QStringList activatedParks = record.field("my_pota_ref") + .value() + .toString() + .split(QRegularExpression("\\s*,\\s*"), + Qt::SplitBehaviorFlags::SkipEmptyParts); + + if (activatedParks.length() <= 0) { + return QList(); + } else if (activatedParks.length() == 1) { + return QList({record}); + } else { + QList records = QList(); + for (const QString &parkID : activatedParks) { + QSqlRecord single = QSqlRecord(record); + single.setValue("my_pota_ref", parkID); + + // If this is a park to park - the remote park can also be a multi park + // activation. These must also be split into multiple records. + QSqlField parkToPark = record.field("pota_ref"); + if (parkToPark.isNull() || !parkToPark.value().toString().contains(",")) { + records.append(single); + } else { + QStringList remoteParks + = parkToPark.value().toString().split(QRegularExpression("\\s*,\\s*"), + Qt::SplitBehaviorFlags::SkipEmptyParts); + for (const QString &remoteParkID : remoteParks) { + QSqlRecord remoteSingle = QSqlRecord(single); + remoteSingle.setValue("pota_ref", remoteParkID); + records.append(remoteSingle); + } + } + } + return records; + } +} + +void PotaAdiFormat::exportEnd() +{ + for (AdiFormat *parkFormat : parkFormats.values()) { + parkFormat->exportEnd(); + } +} + +void PotaAdiFormat::duplicateField(QSqlRecord &record, + const QString &fromFieldName, + const QString &toFieldName) +{ + QSqlField dupped(record.field(fromFieldName)); + record.remove(record.indexOf(toFieldName)); + dupped.setName(toFieldName); + record.append(dupped); +} + +PotaAdiFormat::~PotaAdiFormat() +{ + FCT_IDENTIFICATION; + qDeleteAll(parkFormats.values()); + parkFormats.clear(); + qDeleteAll(parkFiles.values()); + parkFiles.clear(); +} diff --git a/logformat/PotaAdiFormat.h b/logformat/PotaAdiFormat.h new file mode 100644 index 00000000..355b6946 --- /dev/null +++ b/logformat/PotaAdiFormat.h @@ -0,0 +1,37 @@ +#ifndef QLOG_LOGFORMAT_POTALOGFORMAT_H +#define QLOG_LOGFORMAT_POTALOGFORMAT_H +#include "AdiFormat.h" + +/* + * A specialized case of ADI export, where each activated park gets T'd into its + * own file with some denormalization and values set to satisfy the pota.app + * upload processes. + */ +class PotaAdiFormat : public AdiFormat +{ +public: + explicit PotaAdiFormat(QTextStream &stream); + + virtual void exportContact(const QSqlRecord &, + QMap *applTags = nullptr) override; + virtual void exportEnd() override; + + virtual bool importNext(QSqlRecord &) override { return false; } + + void setExportInfo(QFile &exportFile); + ~PotaAdiFormat(); + + static QList splitActivatedParks(const QSqlRecord &); + +private: + QFileInfo *exportInfo; + QMap parkFormats; + QMap parkFiles; + QDateTime currentDate; + AdiFormat *getParkFile(const QSqlRecord &record); + static void duplicateField(QSqlRecord &record, + const QString &fromFieldName, + const QString &toFieldName); +}; + +#endif // QLOG_LOGFORMAT_POTALOGFORMAT_H diff --git a/ui/ExportDialog.cpp b/ui/ExportDialog.cpp index 51f6f38f..267c8bf2 100644 --- a/ui/ExportDialog.cpp +++ b/ui/ExportDialog.cpp @@ -1,9 +1,10 @@ -#include +#include "ui/ExportDialog.h" #include +#include #include #include -#include "ui/ExportDialog.h" #include "ui_ExportDialog.h" +#include #include "core/debug.h" #include "models/SqlListModel.h" @@ -142,7 +143,7 @@ void ExportDialog::runExport() QTextStream out(&file); - LogFormat* format = LogFormat::open(ui->typeSelect->currentText(), out); + LogFormat *format = LogFormat::open(ui->typeSelect->currentText(), out); if (!format) { @@ -150,6 +151,10 @@ void ExportDialog::runExport() return; } + if (PotaAdiFormat *potaFormat = dynamic_cast(format)) { + potaFormat->setExportInfo(file); + } + if ( ui->dateRangeCheckBox->isChecked() ) format->setFilterDateRange(ui->startDateEdit->date(), ui->endDateEdit->date()); @@ -334,6 +339,7 @@ void ExportDialog::fillExportedColumnsCombo() ui->exportedColumnsCombo->addItem(tr("All"), "all"); ui->exportedColumnsCombo->addItem(tr("Minimal"), "min"); + ui->exportedColumnsCombo->addItem(tr("POTA"), "pota"); ui->exportedColumnsCombo->addItem(tr("QSL-specific"), "qsl"); ui->exportedColumnsCombo->addItem(tr("Custom 1"), "c1"); ui->exportedColumnsCombo->addItem(tr("Custom 2"), "c2"); @@ -402,6 +408,10 @@ void ExportDialog::exportedColumnsComboChanged(int index) { exportedColumns = settings.value("export/" + comboValue, QVariant::fromValue(qslColumns)).value>(); } + else if ( comboValue == "pota" ) + { + exportedColumns = potaColumns; + } } } @@ -418,12 +428,20 @@ void ExportDialog::fillQSLSendViaCombo() FCT_IDENTIFICATION; QMapIterator iter(Data::instance()->qslSentViaEnum); - int iter_index = 0; - while ( iter.hasNext() ) - { + while (iter.hasNext()) { iter.next(); ui->qslSendViaComboBox->addItem(iter.value(), iter.key()); - iter_index++; + } +} + +void ExportDialog::exportFormatChanged(const QString &format) +{ + FCT_IDENTIFICATION; + + if (format == "POTA") { + ui->exportedColumnsCombo->setCurrentIndex(ui->exportedColumnsCombo->findData("pota")); + } else { + ui->exportedColumnsCombo->setCurrentIndex(ui->exportedColumnsCombo->findData("all")); } } diff --git a/ui/ExportDialog.h b/ui/ExportDialog.h index ca9668fd..e63e1323 100644 --- a/ui/ExportDialog.h +++ b/ui/ExportDialog.h @@ -38,7 +38,7 @@ public slots: void exportedColumnStateChanged(int index, bool state); void exportTypeChanged(int index); void exportedColumnsComboChanged(int); - + void exportFormatChanged(const QString &format); private: Ui::ExportDialog *ui; LogLocale locale; @@ -59,6 +59,23 @@ public slots: LogbookModel::COLUMN_RST_SENT, LogbookModel::COLUMN_RST_RCVD }; + const QSet potaColumns{ + LogbookModel::COLUMN_TIME_ON, + LogbookModel::COLUMN_CALL, + LogbookModel::COLUMN_OPERATOR, + LogbookModel::COLUMN_STATION_CALLSIGN, + LogbookModel::COLUMN_FREQUENCY, + LogbookModel::COLUMN_MODE, + LogbookModel::COLUMN_SUBMODE, + LogbookModel::COLUMN_MY_STATE, + LogbookModel::COLUMN_MY_COUNTRY, + LogbookModel::COLUMN_MY_POTA_REF, + LogbookModel::COLUMN_POTA_REF, + LogbookModel::COLUMN_MY_SIG, + LogbookModel::COLUMN_MY_SIG_INFO, + LogbookModel::COLUMN_SIG, + LogbookModel::COLUMN_SIG_INFO + }; LogbookModel logbookmodel; QSettings settings; const QList qsos4export; diff --git a/ui/ExportDialog.ui b/ui/ExportDialog.ui index 71e437c2..2aee4b6c 100644 --- a/ui/ExportDialog.ui +++ b/ui/ExportDialog.ui @@ -32,7 +32,7 @@ - Qt::ClickFocus + Qt::FocusPolicy::ClickFocus true @@ -78,6 +78,11 @@ JSON + + + POTA + + @@ -210,7 +215,7 @@ - Qt::ClickFocus + Qt::FocusPolicy::ClickFocus Export only QSOs that match the selected date range @@ -232,7 +237,7 @@ - - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -248,7 +253,7 @@ - Qt::ClickFocus + Qt::FocusPolicy::ClickFocus Export only QSOs that match the selected date range @@ -261,7 +266,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -473,10 +478,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Close|QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Close|QDialogButtonBox::StandardButton::Ok @@ -713,6 +718,22 @@ + + typeSelect + currentTextChanged(QString) + ExportDialog + exportFormatChanged(QString) + + + 397 + 46 + + + 232 + 290 + + + toggleDateRange() @@ -721,6 +742,7 @@ toggleMyCallsign() toggleMyGridsquare() myCallsignChanged(QString) + exportFormatChanged(QString) showColumnsSetting() toggleQslSendVia() exportTypeChanged(int)