diff --git a/CMakeLists.txt b/CMakeLists.txt index bb4764e6f..3d9a8d89c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -222,6 +222,7 @@ set (wsjt_qt_CXXSRCS widgets/DoubleClickablePushButton.cpp widgets/DoubleClickableRadioButton.cpp Network/LotWUsers.cpp + Network/FileDownload.cpp models/DecodeHighlightingModel.cpp widgets/DecodeHighlightingListView.cpp models/FoxLog.cpp diff --git a/Configuration.cpp b/Configuration.cpp index 632537b8e..196a5c1ac 100644 --- a/Configuration.cpp +++ b/Configuration.cpp @@ -201,6 +201,7 @@ #include "models/DecodeHighlightingModel.hpp" #include "logbook/logbook.h" #include "widgets/LazyFillComboBox.hpp" +#include "Network/FileDownload.hpp" #include "ui_Configuration.h" #include "moc_Configuration.cpp" @@ -564,6 +565,9 @@ private: Q_SLOT void on_add_macro_line_edit_editingFinished (); Q_SLOT void delete_macro (); void delete_selected_macros (QModelIndexList); + void after_CTY_downloaded(); + void set_CTY_DAT_version(QString const& version); + void error_during_CTY_download (QString const& reason); Q_SLOT void on_udp_server_line_edit_textChanged (QString const&); Q_SLOT void on_udp_server_line_edit_editingFinished (); Q_SLOT void on_save_path_select_push_button_clicked (bool); @@ -574,7 +578,9 @@ private: Q_SLOT void handle_transceiver_failure (QString const& reason); Q_SLOT void on_reset_highlighting_to_defaults_push_button_clicked (bool); Q_SLOT void on_rescan_log_push_button_clicked (bool); + Q_SLOT void on_CTY_download_button_clicked (bool); Q_SLOT void on_LotW_CSV_fetch_push_button_clicked (bool); + Q_SLOT void on_cbx2ToneSpacing_clicked(bool); Q_SLOT void on_cbx4ToneSpacing_clicked(bool); Q_SLOT void on_prompt_to_log_check_box_clicked(bool); @@ -746,7 +752,7 @@ private: QAudioDeviceInfo next_audio_output_device_; AudioDevice::Channel audio_output_channel_; AudioDevice::Channel next_audio_output_channel_; - + FileDownload cty_download; friend class Configuration; }; @@ -859,6 +865,11 @@ bool Configuration::highlight_73 () const {return m_->highlight_73_;} bool Configuration::highlight_DXcall () const {return m_->highlight_DXcall_;} bool Configuration::highlight_DXgrid () const {return m_->highlight_DXgrid_;} +void Configuration::set_CTY_DAT_version(QString const& version) +{ + m_->set_CTY_DAT_version(version); +} + void Configuration::set_calibration (CalibrationParams params) { m_->calibration_ = params; @@ -1183,8 +1194,13 @@ Configuration::impl::impl (Configuration * self, QNetworkAccessManager * network // set up LoTW users CSV file fetching connect (&lotw_users_, &LotWUsers::load_finished, [this] () { - ui_->LotW_CSV_fetch_push_button->setEnabled (true); - }); + ui_->LotW_CSV_fetch_push_button->setEnabled (true); + }); + + connect(&lotw_users_, &LotWUsers::progress, [this] (QString const& msg) { + ui_->LotW_CSV_status_label->setText(msg); + }); + lotw_users_.set_local_file_path (writeable_data_dir_.absoluteFilePath ("lotw-user-activity.csv")); // @@ -2414,9 +2430,45 @@ void Configuration::impl::on_reset_highlighting_to_defaults_push_button_clicked void Configuration::impl::on_rescan_log_push_button_clicked (bool /*clicked*/) { - if (logbook_) logbook_->rescan (); + if (logbook_) { + logbook_->rescan (); + } } +void Configuration::impl::on_CTY_download_button_clicked (bool /*clicked*/) +{ + ui_->CTY_download_button->setEnabled (false); // disable button until download is complete + QDir dataPath {QStandardPaths::writableLocation (QStandardPaths::DataLocation)}; + cty_download.configure(network_manager_, + "http://www.country-files.com/bigcty/cty.dat", + dataPath.absoluteFilePath("cty.dat"), + "WSJT-X CTY Downloader"); + + // set up LoTW users CSV file fetching + connect (&cty_download, &FileDownload::complete, this, &Configuration::impl::after_CTY_downloaded, Qt::UniqueConnection); + connect (&cty_download, &FileDownload::error, this, &Configuration::impl::error_during_CTY_download, Qt::UniqueConnection); + + cty_download.start_download(); +} +void Configuration::impl::set_CTY_DAT_version(QString const& version) +{ + ui_->CTY_file_label->setText(QString{"CTY File Version: %1"}.arg(version)); +} + +void Configuration::impl::error_during_CTY_download (QString const& reason) +{ + MessageBox::warning_message (this, tr ("Error Loading CTY.DAT"), reason); + after_CTY_downloaded(); +} + +void Configuration::impl::after_CTY_downloaded () +{ + ui_->CTY_download_button->setEnabled (true); + if (logbook_) { + logbook_->rescan (); + ui_->CTY_file_label->setText(QString{"CTY File Version: %1"}.arg(logbook_->cty_version())); + } +} void Configuration::impl::on_LotW_CSV_fetch_push_button_clicked (bool /*checked*/) { lotw_users_.load (ui_->LotW_CSV_URL_line_edit->text (), true, true); diff --git a/Configuration.hpp b/Configuration.hpp index 13c54372b..bb2322c3d 100644 --- a/Configuration.hpp +++ b/Configuration.hpp @@ -244,6 +244,8 @@ public: // Close down connection to rig. void transceiver_offline (); + void set_CTY_DAT_version(QString const& version); + // Set transceiver frequency in Hertz. Q_SLOT void transceiver_frequency (Frequency); diff --git a/Configuration.ui b/Configuration.ui index 836b1b635..b3d38dda8 100644 --- a/Configuration.ui +++ b/Configuration.ui @@ -6,8 +6,8 @@ 0 0 - 588 - 642 + 684 + 662 @@ -2407,8 +2407,14 @@ Right click for insert and delete options. Logbook of the World User Validation - - + + + 4 + + + 4 + + Users CSV file URL: @@ -2418,7 +2424,7 @@ Right click for insert and delete options. - + @@ -2445,7 +2451,7 @@ Right click for insert and delete options. - + Age of last upload less than: @@ -2455,27 +2461,57 @@ Right click for insert and delete options. - - - - <html><head/><body><p>Adjust this spin box to set the age threshold of LotW user's last upload date that is accepted as a current LotW user.</p></body></html> - - - Days since last upload - - - days - - - 0 - - - 9999 - - - 365 - - + + + + + + <html><head/><body><p>Adjust this spin box to set the age threshold of LotW user's last upload date that is accepted as a current LotW user.</p></body></html> + + + Days since last upload + + + days + + + 0 + + + 9999 + + + 365 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 240 + 0 + + + + + + + + @@ -2493,6 +2529,35 @@ Right click for insert and delete options. + + + + CTY File Download + + + + 4 + + + 4 + + + + + CTY File Version: + + + + + + + Download Latest CTY.dat + + + + + + @@ -2501,7 +2566,7 @@ Right click for insert and delete options. 20 - 40 + 20 diff --git a/Network/FileDownload.cpp b/Network/FileDownload.cpp new file mode 100644 index 000000000..3ab2a7c6c --- /dev/null +++ b/Network/FileDownload.cpp @@ -0,0 +1,229 @@ + +#include "FileDownload.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include "qt_helpers.hpp" +#include "Logger.hpp" + +FileDownload::FileDownload() : QObject(nullptr) +{ + redirect_count_ = 0; + url_valid_ = false; +} + +FileDownload::~FileDownload() +{ +} +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) +void FileDownload::errorOccurred(QNetworkReply::NetworkError code) +{ + LOG_INFO(QString{"FileDownload [%1]: errorOccurred %2 -> %3"}.arg(user_agent_).arg(code).arg(reply_->errorString())); + Q_EMIT error (reply_->errorString ()); + destfile_.cancelWriting (); + destfile_.commit (); +} +#else +void FileDownload::obsoleteError() +{ + LOG_INFO(QString{"FileDownload [%1]: error -> %3"}.arg(user_agent_).arg(reply_->errorString())); + Q_EMIT error (reply_->errorString ()); + destfile_.cancelWriting (); + destfile_.commit (); +} +#endif + +void FileDownload::configure(QNetworkAccessManager *network_manager, const QString &source_url, const QString &destination_path, const QString &user_agent) +{ + manager_ = network_manager; + source_url_ = source_url; + destination_filename_ = destination_path; + user_agent_ = user_agent; +} + +void FileDownload::store() +{ + if (destfile_.isOpen()) + destfile_.write (reply_->read (reply_->bytesAvailable ())); + else + LOG_INFO(QString{ "FileDownload [%1]: file is not open."}.arg(user_agent_)); +} + +void FileDownload::replyComplete() +{ + QFileInfo destination_file(destination_filename_); + QDir tmpdir_(destination_file.absoluteFilePath()); + + LOG_DEBUG(QString{ "FileDownload [%1]: replyComplete"}.arg(user_agent_)); + if (!reply_) + { + Q_EMIT load_finished (); + return; // we probably deleted it in an earlier call + } + + QUrl redirect_url {reply_->attribute (QNetworkRequest::RedirectionTargetAttribute).toUrl ()}; + + if (reply_->error () == QNetworkReply::NoError && !redirect_url.isEmpty ()) + { + if ("https" == redirect_url.scheme () && !QSslSocket::supportsSsl ()) + { + Q_EMIT download_error (tr ("Network Error - SSL/TLS support not installed, cannot fetch:\n\'%1\'") + .arg (redirect_url.toDisplayString ())); + url_valid_ = false; // reset + Q_EMIT load_finished (); + } + else if (++redirect_count_ < 10) // maintain sanity + { + // follow redirect + download (reply_->url ().resolved (redirect_url)); + } + else + { + Q_EMIT download_error (tr ("Network Error - Too many redirects:\n\'%1\'") + .arg (redirect_url.toDisplayString ())); + url_valid_ = false; // reset + Q_EMIT load_finished (); + } + } + else if (reply_->error () != QNetworkReply::NoError) + { + destfile_.cancelWriting(); + destfile_.commit(); + url_valid_ = false; // reset + // report errors that are not due to abort + if (QNetworkReply::OperationCanceledError != reply_->error ()) + { + Q_EMIT download_error (tr ("Network Error:\n%1") + .arg (reply_->errorString ())); + } + Q_EMIT load_finished (); + } + else + { + if (!url_valid_) + { + // now get the body content + url_valid_ = true; + download (reply_->url ().resolved (redirect_url)); + } + else // the body has completed. Save it. + { + url_valid_ = false; // reset + // load the database asynchronously + // future_load_ = std::async (std::launch::async, &LotWUsers::impl::load_dictionary, this, csv_file_.fileName ()); + LOG_INFO(QString{ "FileDownload [%1]: complete. File path is %2"}.arg(user_agent_).arg(destfile_.fileName())); + destfile_.commit(); + emit complete(destination_filename_); + } + } + + if (reply_ && reply_->isFinished ()) + { + reply_->deleteLater (); + } + +} + +void FileDownload::downloadComplete(QNetworkReply *data) +{ + // make a temp file in the same place as the file we're downloading. Needs to be on the same + // filesystem as where we eventually want to 'mv' it. + + QUrl r = request_.url(); + LOG_INFO(QString{"FileDownload [%1]: finished %2 of %3 -> %4 (%5)"}.arg(user_agent_).arg(data->operation()).arg(source_url_).arg(destination_filename_).arg(r.url())); + +#ifdef DEBUG_FILEDOWNLOAD + LOG_INFO("Request Headers:"); + Q_FOREACH (const QByteArray& hdr, request_.rawHeaderList()) { + LOG_INFO(QString{ "%1 -> %2"}.arg(QString(hdr)).arg(QString(request_.rawHeader(hdr)))); + } + + LOG_INFO("Response Headers:"); + Q_FOREACH (const QByteArray& hdr, reply_->rawHeaderList()) { + LOG_INFO(QString{ "%1 -> %2"}.arg(QString(hdr)).arg(QString(reply_->rawHeader(hdr)))); + } +#endif + data->deleteLater(); +} + +void FileDownload::start_download() +{ + url_valid_ = false; + download(QUrl(source_url_)); +} + +void FileDownload::download(QUrl qurl) +{ + request_.setUrl(qurl); + +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + if (QNetworkAccessManager::Accessible != manager_->networkAccessible ()) + { + // try and recover network access for QNAM + manager_->setNetworkAccessible (QNetworkAccessManager::Accessible); + } +#endif + + LOG_INFO(QString{"FileDownload [%1]: Starting download of %2 to %3"}.arg(user_agent_).arg(source_url_).arg(destination_filename_)); + + request_.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + request_.setRawHeader("Accept", "*/*"); + request_.setRawHeader ("User-Agent", user_agent_.toLocal8Bit()); // Must have a UA for some sites, like country-files + + if (!url_valid_) + { + reply_ = manager_->head(request_); + } + else + { + reply_ = manager_->get (request_); + } + + QObject::connect(manager_, &QNetworkAccessManager::finished, this, &FileDownload::downloadComplete, Qt::UniqueConnection); + QObject::connect(reply_, &QNetworkReply::downloadProgress, this, &FileDownload::downloadProgress, Qt::UniqueConnection); + QObject::connect(reply_, &QNetworkReply::finished, this, &FileDownload::replyComplete, Qt::UniqueConnection); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + QObject::connect(reply_, &QNetworkReply::errorOccurred,this, &FileDownload::errorOccurred, Qt::UniqueConnection); +#else + QObject::connect(reply_, QOverload::of(&QNetworkReply::error), this, &FileDownload::obsoleteError, Qt::UniqueConnection); +#endif + QObject::connect(reply_, &QNetworkReply::readyRead, this, &FileDownload::store, Qt::UniqueConnection); + + QFileInfo destination_file(destination_filename_); + QString const tmpfile_base = destination_file.fileName(); + QString const &tmpfile_path = destination_file.absolutePath(); + QDir tmpdir{}; + if (!tmpdir.mkpath(tmpfile_path)) + { + LOG_INFO(QString{"FileDownload [%1]: Directory %2 does not exist"}.arg(user_agent_).arg(tmpfile_path).arg( + destfile_.errorString())); + } + + if (url_valid_) { + destfile_.setFileName(destination_file.absoluteFilePath()); + if (!destfile_.open(QSaveFile::WriteOnly | QIODevice::WriteOnly)) { + LOG_INFO(QString{"FileDownload [%1]: Unable to open %2: %3"}.arg(user_agent_).arg(destfile_.fileName()).arg( + destfile_.errorString())); + return; + } + } +} + +void FileDownload::downloadProgress(qint64 received, qint64 total) +{ + LOG_DEBUG(QString{"FileDownload: [%1] Progress %2 from %3, total %4, so far %5"}.arg(user_agent_).arg(destination_filename_).arg(source_url_).arg(total).arg(received)); + Q_EMIT progress(QString{"%4 bytes downloaded"}.arg(received)); +} + +void FileDownload::abort () +{ + if (reply_ && reply_->isRunning ()) + { + reply_->abort (); + } +} diff --git a/Network/FileDownload.hpp b/Network/FileDownload.hpp new file mode 100644 index 000000000..c32948dd7 --- /dev/null +++ b/Network/FileDownload.hpp @@ -0,0 +1,54 @@ +#ifndef WSJTX_FILEDOWNLOAD_H +#define WSJTX_FILEDOWNLOAD_H + +#include +#include +#include +#include +#include +#include +#include + +class FileDownload : public QObject { + Q_OBJECT + +public: + explicit FileDownload(); + ~FileDownload(); + + void configure(QNetworkAccessManager *network_manager, const QString& source_url, const QString& destination_filename, const QString& user_agent); + +private: + QNetworkAccessManager *manager_; + QString source_url_; + QString destination_filename_; + QString user_agent_; + QPointer reply_; + QNetworkRequest request_; + QSaveFile destfile_; + bool url_valid_; + int redirect_count_; +signals: + void complete(QString filename); + void progress(QString filename); + void load_finished() const; + void download_error (QString const& reason) const; + void error(QString const& reason) const; + + +public slots: + void start_download(); + void download(QUrl url); + void store(); + void abort(); + void downloadComplete(QNetworkReply* data); + void downloadProgress(qint64 recieved, qint64 total); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + void errorOccurred(QNetworkReply::NetworkError code); +#else + void obsoleteError(); +#endif + void replyComplete(); +}; + +#endif //WSJTX_FILEDOWNLOAD_H diff --git a/Network/LotWUsers.cpp b/Network/LotWUsers.cpp index 4e8024010..10a9a4d8b 100644 --- a/Network/LotWUsers.cpp +++ b/Network/LotWUsers.cpp @@ -16,7 +16,9 @@ #include #include #include - +#include "qt_helpers.hpp" +#include "Logger.hpp" +#include "FileDownload.hpp" #include "pimpl_impl.hpp" #include "moc_LotWUsers.cpp" @@ -39,6 +41,7 @@ public: , url_valid_ {false} , redirect_count_ {0} , age_constraint_ {365} + , connected_ {false} { } @@ -48,14 +51,36 @@ public: auto csv_file_name = csv_file_.fileName (); auto exists = QFileInfo::exists (csv_file_name); if (fetch && (!exists || forced_fetch)) + { + current_url_.setUrl(url); + if (current_url_.isValid() && !QSslSocket::supportsSsl()) { - current_url_.setUrl (url); - if (current_url_.isValid () && !QSslSocket::supportsSsl ()) - { - current_url_.setScheme ("http"); - } - redirect_count_ = 0; - download (current_url_); + current_url_.setScheme("http"); + } + redirect_count_ = 0; + + Q_EMIT self_->progress (QString("Starting download from %1").arg(url)); + + lotw_downloader_.configure(network_manager_, + url, + csv_file_name, + "WSJT-X LotW User Downloader"); + if (!connected_) + { + connect(&lotw_downloader_, &FileDownload::complete, [this, csv_file_name] { + LOG_INFO(QString{"LotWUsers: Loading LotW file %1"}.arg(csv_file_name)); + future_load_ = std::async(std::launch::async, &LotWUsers::impl::load_dictionary, this, csv_file_name); + }); + connect(&lotw_downloader_, &FileDownload::error, [this] (QString const& msg) { + LOG_INFO(QString{"LotWUsers: Error downloading LotW file: %1"}.arg(msg)); + Q_EMIT self_->LotW_users_error (msg); + }); + connect( &lotw_downloader_, &FileDownload::progress, [this] (QString const& msg) { + Q_EMIT self_->progress (msg); + }); + connected_ = true; + } + lotw_downloader_.start_download(); } else { @@ -67,142 +92,9 @@ public: } } - void download (QUrl url) - { -#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) - if (QNetworkAccessManager::Accessible != network_manager_->networkAccessible ()) - { - // try and recover network access for QNAM - network_manager_->setNetworkAccessible (QNetworkAccessManager::Accessible); - } -#endif - - QNetworkRequest request {url}; - request.setRawHeader ("User-Agent", "WSJT LotW User Downloader"); - request.setOriginatingObject (this); - - // this blocks for a second or two the first time it is used on - // Windows - annoying - if (!url_valid_) - { - reply_ = network_manager_->head (request); - } - else - { - reply_ = network_manager_->get (request); - } - - connect (reply_.data (), &QNetworkReply::finished, this, &LotWUsers::impl::reply_finished); - connect (reply_.data (), &QNetworkReply::readyRead, this, &LotWUsers::impl::store); - } - - void reply_finished () - { - if (!reply_) - { - Q_EMIT self_->load_finished (); - return; // we probably deleted it in an earlier call - } - QUrl redirect_url {reply_->attribute (QNetworkRequest::RedirectionTargetAttribute).toUrl ()}; - if (reply_->error () == QNetworkReply::NoError && !redirect_url.isEmpty ()) - { - if ("https" == redirect_url.scheme () && !QSslSocket::supportsSsl ()) - { - Q_EMIT self_->LotW_users_error (tr ("Network Error - SSL/TLS support not installed, cannot fetch:\n\'%1\'") - .arg (redirect_url.toDisplayString ())); - url_valid_ = false; // reset - Q_EMIT self_->load_finished (); - } - else if (++redirect_count_ < 10) // maintain sanity - { - // follow redirect - download (reply_->url ().resolved (redirect_url)); - } - else - { - Q_EMIT self_->LotW_users_error (tr ("Network Error - Too many redirects:\n\'%1\'") - .arg (redirect_url.toDisplayString ())); - url_valid_ = false; // reset - Q_EMIT self_->load_finished (); - } - } - else if (reply_->error () != QNetworkReply::NoError) - { - csv_file_.cancelWriting (); - csv_file_.commit (); - url_valid_ = false; // reset - // report errors that are not due to abort - if (QNetworkReply::OperationCanceledError != reply_->error ()) - { - Q_EMIT self_->LotW_users_error (tr ("Network Error:\n%1") - .arg (reply_->errorString ())); - } - Q_EMIT self_->load_finished (); - } - else - { - if (url_valid_ && !csv_file_.commit ()) - { - Q_EMIT self_->LotW_users_error (tr ("File System Error - Cannot commit changes to:\n\"%1\"") - .arg (csv_file_.fileName ())); - url_valid_ = false; // reset - Q_EMIT self_->load_finished (); - } - else - { - if (!url_valid_) - { - // now get the body content - url_valid_ = true; - download (reply_->url ().resolved (redirect_url)); - } - else - { - url_valid_ = false; // reset - // load the database asynchronously - future_load_ = std::async (std::launch::async, &LotWUsers::impl::load_dictionary, this, csv_file_.fileName ()); - } - } - } - if (reply_ && reply_->isFinished ()) - { - reply_->deleteLater (); - } - } - - void store () - { - if (url_valid_) - { - if (!csv_file_.isOpen ()) - { - // create temporary file in the final location - if (!csv_file_.open (QSaveFile::WriteOnly)) - { - abort (); - Q_EMIT self_->LotW_users_error (tr ("File System Error - Cannot open file:\n\"%1\"\nError(%2): %3") - .arg (csv_file_.fileName ()) - .arg (csv_file_.error ()) - .arg (csv_file_.errorString ())); - } - } - if (csv_file_.write (reply_->read (reply_->bytesAvailable ())) < 0) - { - abort (); - Q_EMIT self_->LotW_users_error (tr ("File System Error - Cannot write to file:\n\"%1\"\nError(%2): %3") - .arg (csv_file_.fileName ()) - .arg (csv_file_.error ()) - .arg (csv_file_.errorString ())); - } - } - } - void abort () { - if (reply_ && reply_->isRunning ()) - { - reply_->abort (); - } + lotw_downloader_.abort(); } // Load the database from the given file name @@ -222,12 +114,14 @@ public: auto pos = l.indexOf (','); result[l.left (pos)] = QDate::fromString (l.mid (pos + 1, l.indexOf (',', pos + 1) - pos - 1), "yyyy-MM-dd"); } -// qDebug () << "LotW User Data Loaded"; } else { throw std::runtime_error {QObject::tr ("Failed to open LotW users CSV file: '%1'").arg (f.fileName ()).toStdString ()}; } + LOG_INFO(QString{"LotWUsers: Loaded %1 records from %2"}.arg(result.size()).arg(lotw_csv_file)); + Q_EMIT self_->progress (QString{"Loaded %1 records from LotW."}.arg(result.size())); + Q_EMIT self_->load_finished(); return result; } @@ -241,6 +135,8 @@ public: std::future future_load_; dictionary last_uploaded_; qint64 age_constraint_; // days + FileDownload lotw_downloader_; + bool connected_; }; #include "LotWUsers.moc" @@ -249,6 +145,7 @@ LotWUsers::LotWUsers (QNetworkAccessManager * network_manager, QObject * parent) : QObject {parent} , m_ {this, network_manager} { + } LotWUsers::~LotWUsers () diff --git a/Network/LotWUsers.hpp b/Network/LotWUsers.hpp index 238c57402..2d4d13075 100644 --- a/Network/LotWUsers.hpp +++ b/Network/LotWUsers.hpp @@ -31,6 +31,7 @@ public: bool user (QString const& call) const; Q_SIGNAL void LotW_users_error (QString const& reason) const; + Q_SIGNAL void progress (QString const& reason) const; Q_SIGNAL void load_finished () const; private: diff --git a/logbook/AD1CCty.cpp b/logbook/AD1CCty.cpp index 4112ddfb3..526c6c8df 100644 --- a/logbook/AD1CCty.cpp +++ b/logbook/AD1CCty.cpp @@ -16,9 +16,11 @@ #include #include #include +#include #include "Configuration.hpp" #include "Radio.hpp" #include "pimpl_impl.hpp" +#include "Logger.hpp" #include "moc_AD1CCty.cpp" @@ -163,6 +165,9 @@ public: { } + QString get_cty_path(const Configuration *configuration); + void load_cty(QFile &file); + entity_by_id::iterator lookup_entity (QString call, prefix const& p) const { call = call.toUpper (); @@ -228,6 +233,9 @@ public: Configuration const * configuration_; QString path_; + QString cty_version_; + QString cty_version_date_; + entities_type entities_; prefixes_type prefixes_; }; @@ -314,6 +322,80 @@ char const * AD1CCty::continent (Continent c) } } +QString AD1CCty::impl::get_cty_path(Configuration const * configuration) +{ + QDir dataPath {QStandardPaths::writableLocation (QStandardPaths::DataLocation)}; + auto path = dataPath.exists (file_name) + ? dataPath.absoluteFilePath (file_name) // user override + : configuration->data_dir ().absoluteFilePath (file_name); // or original + return path; +} + +void AD1CCty::impl::load_cty(QFile &file) +{ + QRegularExpression version_pattern{R"(VER\d{8})"}; + int entity_id = 0; + int line_number{0}; + + entities_.clear(); + prefixes_.clear(); + cty_version_ = QString{}; + cty_version_date_ = QString{}; + + QTextStream in{&file}; + while (!in.atEnd()) + { + auto const &entity_line = in.readLine(); + ++line_number; + if (!in.atEnd()) + { + auto const &entity_parts = entity_line.split(':'); + if (entity_parts.size() >= 8) + { + auto primary_prefix = entity_parts[7].trimmed(); + bool WAE_only{false}; + if (primary_prefix.startsWith('*')) + { + primary_prefix = primary_prefix.mid(1); + WAE_only = true; + } + bool ok1, ok2, ok3, ok4, ok5; + entities_.emplace(++entity_id, entity_parts[0].trimmed(), WAE_only, entity_parts[1].trimmed().toInt(&ok1), + entity_parts[2].trimmed().toInt(&ok2), continent(entity_parts[3].trimmed()), + entity_parts[4].trimmed().toFloat(&ok3), entity_parts[5].trimmed().toFloat(&ok4), + static_cast (entity_parts[6].trimmed().toFloat(&ok5) * 60 * 60), primary_prefix); + if (!(ok1 && ok2 && ok3 && ok4 && ok5)) + { + throw std::domain_error{"Invalid number in cty.dat line " + boost::lexical_cast(line_number)}; + } + QString line; + QString detail; + do + { + in.readLineInto(&line); + ++line_number; + } while (detail += line, !detail.endsWith(';')); + for (auto prefix: detail.left(detail.size() - 1).split(',')) + { + prefix = prefix.trimmed(); + bool exact{false}; + if (prefix.startsWith('=')) + { + prefix = prefix.mid(1); + exact = true; + // match version pattern to prefix + if (version_pattern.match(prefix).hasMatch()) + { + cty_version_date_ = prefix; + } + } + prefixes_.emplace(prefix, exact, entity_id); + } + } + } + } +} + AD1CCty::AD1CCty (Configuration const * configuration) : m_ {configuration} { @@ -321,69 +403,23 @@ AD1CCty::AD1CCty (Configuration const * configuration) // TODO: G4WJS - consider doing the following asynchronously to // speed up startup. Not urgent as it takes less than 0.5s on a Core // i7 reading BIG CTY.DAT. - QDir dataPath {QStandardPaths::writableLocation (QStandardPaths::DataLocation)}; - m_->path_ = dataPath.exists (file_name) - ? dataPath.absoluteFilePath (file_name) // user override - : configuration->data_dir ().absoluteFilePath (file_name); // or original + AD1CCty::reload (configuration); + } + +void AD1CCty::reload(Configuration const * configuration) +{ + m_->path_ = m_->impl::get_cty_path(configuration); QFile file {m_->path_}; + + LOG_INFO(QString{"Loading CTY.DAT from %1"}.arg (m_->path_)); + if (file.open (QFile::ReadOnly)) - { - int entity_id = 0; - int line_number {0}; - QTextStream in {&file}; - while (!in.atEnd ()) - { - auto const& entity_line = in.readLine (); - ++line_number; - if (!in.atEnd ()) - { - auto const& entity_parts = entity_line.split (':'); - if (entity_parts.size () >= 8) - { - auto primary_prefix = entity_parts[7].trimmed (); - bool WAE_only {false}; - if (primary_prefix.startsWith ('*')) - { - primary_prefix = primary_prefix.mid (1); - WAE_only = true; - } - bool ok1, ok2, ok3, ok4, ok5; - m_->entities_.emplace (++entity_id - , entity_parts[0].trimmed () - , WAE_only - , entity_parts[1].trimmed ().toInt (&ok1) - , entity_parts[2].trimmed ().toInt (&ok2) - , continent (entity_parts[3].trimmed ()) - , entity_parts[4].trimmed ().toFloat (&ok3) - , entity_parts[5].trimmed ().toFloat (&ok4) - , static_cast (entity_parts[6].trimmed ().toFloat (&ok5) * 60 * 60) - , primary_prefix); - if (!(ok1 && ok2 && ok3 && ok4 && ok5)) - { - throw std::domain_error {"Invalid number in cty.dat line " + boost::lexical_cast (line_number)}; - } - QString line; - QString detail; - do - { - in.readLineInto (&line); - ++line_number; - } while (detail += line, !detail.endsWith (';')); - for (auto prefix : detail.left (detail.size () - 1).split (',')) - { - prefix = prefix.trimmed (); - bool exact {false}; - if (prefix.startsWith ('=')) - { - prefix = prefix.mid (1); - exact = true; - } - m_->prefixes_.emplace (prefix, exact, entity_id); - } - } - } - } - } + { + m_->impl::load_cty(file); + m_->cty_version_ = AD1CCty::lookup("VERSION").entity_name; + Q_EMIT cty_loaded(m_->cty_version_); + LOG_INFO(QString{"Loaded CTY.DAT version %1, %2"}.arg (m_->cty_version_date_).arg (m_->cty_version_)); + } } AD1CCty::~AD1CCty () @@ -421,3 +457,7 @@ auto AD1CCty::lookup (QString const& call) const -> Record } return Record {}; } +auto AD1CCty::version () const -> QString +{ + return m_->cty_version_date_; +} diff --git a/logbook/AD1CCty.hpp b/logbook/AD1CCty.hpp index 4a485afa9..982ff7cec 100644 --- a/logbook/AD1CCty.hpp +++ b/logbook/AD1CCty.hpp @@ -42,8 +42,11 @@ public: }; explicit AD1CCty (Configuration const *); + void reload(Configuration const * configuration); ~AD1CCty (); Record lookup (QString const& call) const; + QString version () const; + Q_SIGNAL void cty_loaded (QString const& version) const; private: class impl; diff --git a/logbook/WorkedBefore.cpp b/logbook/WorkedBefore.cpp index e7214d41b..b5dedc3d2 100644 --- a/logbook/WorkedBefore.cpp +++ b/logbook/WorkedBefore.cpp @@ -23,6 +23,7 @@ #include #include "Configuration.hpp" #include "revision_utils.hpp" +#include "Logger.hpp" #include "qt_helpers.hpp" #include "pimpl_impl.hpp" @@ -225,7 +226,7 @@ namespace { auto const logFileName = "wsjtx_log.adi"; - // Expception class suitable for using with QtConcurrent across + // Exception class suitable for using with QtConcurrent across // thread boundaries class LoaderException final : public QException @@ -374,6 +375,7 @@ public: void reload () { + prefixes_.reload (configuration_); async_loader_ = QtConcurrent::run (loader, path_, &prefixes_); loader_watcher_.setFuture (async_loader_); } @@ -402,11 +404,18 @@ WorkedBefore::WorkedBefore (Configuration const * configuration) { error = e.error (); } - Q_EMIT finished_loading (n, error); + QString cty_ver = m_->prefixes_.version(); + LOG_DEBUG(QString{"WorkedBefore::reload: CTY.DAT version %1"}.arg (cty_ver)); + Q_EMIT finished_loading (n, cty_ver, error); }); reload (); } +QString WorkedBefore::cty_version () const +{ + return m_->prefixes_.version (); +} + void WorkedBefore::reload () { m_->reload (); @@ -668,6 +677,7 @@ bool WorkedBefore::CQ_zone_worked (int CQ_zone, QString const& mode, QString con } } + bool WorkedBefore::ITU_zone_worked (int ITU_zone, QString const& mode, QString const& band) const { if (mode.size ()) @@ -699,3 +709,5 @@ bool WorkedBefore::ITU_zone_worked (int ITU_zone, QString const& mode, QString c } } } + + diff --git a/logbook/WorkedBefore.hpp b/logbook/WorkedBefore.hpp index 1aae1aca4..0be9d783f 100644 --- a/logbook/WorkedBefore.hpp +++ b/logbook/WorkedBefore.hpp @@ -36,8 +36,9 @@ public: bool continent_worked (Continent continent, QString const& mode, QString const& band) const; bool CQ_zone_worked (int CQ_zone, QString const& mode, QString const& band) const; bool ITU_zone_worked (int ITU_zone, QString const& mode, QString const& band) const; + QString cty_version () const; - Q_SIGNAL void finished_loading (int worked_before_record_count, QString const& error) const; + Q_SIGNAL void finished_loading (int worked_before_record_count, QString const, QString const& error) const; private: class impl; diff --git a/logbook/logbook.cpp b/logbook/logbook.cpp index 2f2b70a5d..411d6240c 100644 --- a/logbook/logbook.cpp +++ b/logbook/logbook.cpp @@ -69,6 +69,11 @@ void LogBook::rescan () worked_before_.reload (); } +QString const LogBook::cty_version() const +{ + return worked_before_.cty_version(); +} + QByteArray LogBook::QSOToADIF (QString const& hisCall, QString const& hisGrid, QString const& mode, QString const& rptSent, QString const& rptRcvd, QDateTime const& dateTimeOn, QDateTime const& dateTimeOff, QString const& band, QString const& comments, diff --git a/logbook/logbook.h b/logbook/logbook.h index de7ffae10..96a17dd6e 100644 --- a/logbook/logbook.h +++ b/logbook/logbook.h @@ -46,7 +46,9 @@ public: QString const& m_myGrid, QString const& m_txPower, QString const& operator_call, QString const& xSent, QString const& xRcvd, QString const& propmode); - Q_SIGNAL void finished_loading (int worked_before_record_count, QString const& error) const; + QString const cty_version() const; + + Q_SIGNAL void finished_loading (int worked_before_record_count, QString const cty_version, QString const& error) const; CabrilloLog * contest_log (); Multiplier const * multiplier () const; diff --git a/translations/wsjtx_ca.ts b/translations/wsjtx_ca.ts index db7c4fe94..e16426840 100644 --- a/translations/wsjtx_ca.ts +++ b/translations/wsjtx_ca.ts @@ -3684,7 +3684,7 @@ La llista es pot mantenir a la configuració (F2). - Scanned ADIF log, %1 worked before records created + Scanned ADIF log, %1 worked-before records created. CTY: %2 Log ADIF escanejat, %1 funcionava abans de la creació de registres diff --git a/translations/wsjtx_da.ts b/translations/wsjtx_da.ts index f9c632236..51d9d651b 100644 --- a/translations/wsjtx_da.ts +++ b/translations/wsjtx_da.ts @@ -3917,7 +3917,7 @@ listen. Makro listen kan også ændfres i Inderstillinger (F2). - Scanned ADIF log, %1 worked before records created + Scanned ADIF log, %1 worked-before records created. CTY: %2 Scannet ADIF log, %1 worked B4 oprettede poster diff --git a/translations/wsjtx_en.ts b/translations/wsjtx_en.ts index 8d5413a8a..dfec1d917 100644 --- a/translations/wsjtx_en.ts +++ b/translations/wsjtx_en.ts @@ -3637,7 +3637,7 @@ list. The list can be maintained in Settings (F2). - Scanned ADIF log, %1 worked before records created + Scanned ADIF log, %1 worked-before records created. CTY: %2 diff --git a/translations/wsjtx_en_GB.ts b/translations/wsjtx_en_GB.ts index fe223f710..4d1fd1f44 100644 --- a/translations/wsjtx_en_GB.ts +++ b/translations/wsjtx_en_GB.ts @@ -3637,7 +3637,7 @@ list. The list can be maintained in Settings (F2). - Scanned ADIF log, %1 worked before records created + Scanned ADIF log, %1 worked-before records created diff --git a/translations/wsjtx_es.ts b/translations/wsjtx_es.ts index e2cc8caf7..3e79fdf4a 100644 --- a/translations/wsjtx_es.ts +++ b/translations/wsjtx_es.ts @@ -4237,7 +4237,7 @@ predefinida. La lista se puede modificar en "Ajustes" (F2). - Scanned ADIF log, %1 worked before records created + Scanned ADIF log, %1 worked-before records created. CTY: %2 Log ADIF escaneado, %1 funcionaba antes de la creación de registros Log ADIF escaneado, %1 registros trabajados B4 creados diff --git a/translations/wsjtx_it.ts b/translations/wsjtx_it.ts index 52a1ba5bd..ada4b4353 100644 --- a/translations/wsjtx_it.ts +++ b/translations/wsjtx_it.ts @@ -3891,7 +3891,7 @@ elenco. L'elenco può essere gestito in Impostazioni (F2). - Scanned ADIF log, %1 worked before records created + Scanned ADIF log, %1 worked-before records created. CTY: %2 Log ADIF scansionato,%1 ha funzionato prima della creazione dei record diff --git a/translations/wsjtx_ja.ts b/translations/wsjtx_ja.ts index e294cb461..ed9b26fad 100644 --- a/translations/wsjtx_ja.ts +++ b/translations/wsjtx_ja.ts @@ -3869,7 +3869,7 @@ ENTERを押してテキストを登録リストに追加. - Scanned ADIF log, %1 worked before records created + Scanned ADIF log, %1 worked-before records created. CTY: %2 ADIFログ検索. %1交信済み記録作成しました diff --git a/translations/wsjtx_ru.ts b/translations/wsjtx_ru.ts index 409a72a13..1f6bfe189 100644 --- a/translations/wsjtx_ru.ts +++ b/translations/wsjtx_ru.ts @@ -3690,7 +3690,7 @@ list. The list can be maintained in Settings (F2). - Scanned ADIF log, %1 worked before records created + Scanned ADIF log, %1 worked-before records created. CTY: %2 Просканирован лог ADIF, %1 работал до создания записей diff --git a/translations/wsjtx_zh.ts b/translations/wsjtx_zh.ts index 93ef6c078..95f0bcf25 100644 --- a/translations/wsjtx_zh.ts +++ b/translations/wsjtx_zh.ts @@ -3683,7 +3683,7 @@ list. The list can be maintained in Settings (F2). - Scanned ADIF log, %1 worked before records created + Scanned ADIF log, %1 worked-before records created. CTY: %2 扫描 ADIF 日志, %1 创建曾经通联记录 diff --git a/translations/wsjtx_zh_HK.ts b/translations/wsjtx_zh_HK.ts index 86f599a31..0efa2fe31 100644 --- a/translations/wsjtx_zh_HK.ts +++ b/translations/wsjtx_zh_HK.ts @@ -3683,7 +3683,7 @@ list. The list can be maintained in Settings (F2). - Scanned ADIF log, %1 worked before records created + Scanned ADIF log, %1 worked-before records created 掃描 ADIF 紀錄, %1 建立曾經通聯紀錄 diff --git a/translations/wsjtx_zh_TW.ts b/translations/wsjtx_zh_TW.ts index a4b23d7d3..66f27f1e6 100644 --- a/translations/wsjtx_zh_TW.ts +++ b/translations/wsjtx_zh_TW.ts @@ -3714,7 +3714,7 @@ list. The list can be maintained in Settings (F2). - Scanned ADIF log, %1 worked before records created + Scanned ADIF log, %1 worked-before records created. CTY: %2 掃描 ADIF 紀錄, %1 建立曾經通聯紀錄 diff --git a/widgets/mainwindow.cpp b/widgets/mainwindow.cpp index d42b29b76..beeb4b914 100644 --- a/widgets/mainwindow.cpp +++ b/widgets/mainwindow.cpp @@ -551,14 +551,15 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, connect (this, &MainWindow::finished, m_logDlg.data (), &LogQSO::close); // hook up the log book - connect (&m_logBook, &LogBook::finished_loading, [this] (int record_count, QString const& error) { + connect (&m_logBook, &LogBook::finished_loading, [this] (int record_count, QString cty_version, QString const& error) { if (error.size ()) { MessageBox::warning_message (this, tr ("Error Scanning ADIF Log"), error); } else { - showStatusMessage (tr ("Scanned ADIF log, %1 worked before records created").arg (record_count)); + m_config.set_CTY_DAT_version(cty_version); + showStatusMessage (tr ("Scanned ADIF log, %1 worked-before records created. CTY: %2").arg (record_count).arg (cty_version)); } }); @@ -675,6 +676,7 @@ MainWindow::MainWindow(QDir const& temp_directory, bool multiple, MessageBox::warning_message (this, tr ("Error Loading LotW Users Data"), reason); }, Qt::QueuedConnection); + QButtonGroup* txMsgButtonGroup = new QButtonGroup {this}; txMsgButtonGroup->addButton(ui->txrb1,1); txMsgButtonGroup->addButton(ui->txrb2,2); @@ -4204,6 +4206,7 @@ void MainWindow::readFromStdout() //readFromStdout m_rptRcvd=w.at(2); m_rptSent=decodedtext.string().mid(7,3); m_nFoxFreq=decodedtext.string().mid(16,4).toInt(); + hound_reply (); } else { if (text.contains(m_config.my_callsign() + " " + m_hisCall) && !text.contains("73 ")) processMessage(decodedtext0); // needed for MSHV multistream messages } @@ -7834,6 +7837,8 @@ void MainWindow::band_changed (Frequency f) } setRig (f); setXIT (ui->TxFreqSpinBox->value ()); + m_specOp=m_config.special_op_id(); + if (m_specOp==SpecOp::FOX) FoxReset("BandChange"); // when changing bands, don't preserve the Fox queues } } @@ -9575,6 +9580,21 @@ void MainWindow::on_sbMax_dB_valueChanged(int n) t = t.asprintf(" Max_dB %d",m_max_dB); writeFoxQSO(t); } +void MainWindow::FoxReset(QString reason="") +{ + QFile f(m_config.temp_dir().absoluteFilePath("houndcallers.txt")); + f.remove(); + ui->decodedTextBrowser->setText(""); + ui->houndQueueTextBrowser->setText(""); + ui->foxTxListTextBrowser->setText(""); + + m_houndQueue.clear(); + m_foxQSO.clear(); + m_foxQSOinProgress.clear(); + m_discard_decoded_hounds_this_cycle = true; // discard decoded messages until the next cycle + if (reason != "") writeFoxQSO(" " + reason); + writeFoxQSO(" Reset"); +} void MainWindow::on_pbFoxReset_clicked() { @@ -9582,16 +9602,7 @@ void MainWindow::on_pbFoxReset_clicked() auto button = MessageBox::query_message (this, tr ("Confirm Reset"), tr ("Are you sure you want to clear the QSO queues?")); if(button == MessageBox::Yes) { - QFile f(m_config.temp_dir().absoluteFilePath("houndcallers.txt")); - f.remove(); - ui->decodedTextBrowser->setText(""); - ui->houndQueueTextBrowser->setText(""); - ui->foxTxListTextBrowser->setText(""); - - m_houndQueue.clear(); - m_foxQSO.clear(); - m_foxQSOinProgress.clear(); - writeFoxQSO(" Reset"); + FoxReset(); } } @@ -9715,6 +9726,7 @@ void MainWindow::selectHound(QString line, bool bTopQueue) * is equivalent to double-clicking on the top-most line. */ if(line.length()==0) return; + if(line.length() < 6) return; QString houndCall=line.split(" ",SkipEmptyParts).at(0); // Don't add a call already enqueued or in QSO @@ -9759,6 +9771,15 @@ void MainWindow::houndCallers() * Distance, Age, and Continent) to a list, sort the list by specified criteria, * and display the top N_Hounds entries in the left text window. */ + // if frequency was changed in the middle of an interval, there's a flag set to ignore the decodes. Reset it here + // + + if (m_discard_decoded_hounds_this_cycle) + { + m_discard_decoded_hounds_this_cycle = false; // + return; // don't use these decodes + } + QFile f(m_config.temp_dir().absoluteFilePath("houndcallers.txt")); if(f.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream s(&f); @@ -9974,26 +9995,35 @@ list2Done: } if(hc1!="") { - // Log this QSO! - auto QSO_time = QDateTime::currentDateTimeUtc (); - m_hisCall=hc1; - m_hisGrid=m_foxQSO[hc1].grid; - m_rptSent=m_foxQSO[hc1].sent; - m_rptRcvd=m_foxQSO[hc1].rcvd; - if (!m_foxLogWindow) on_fox_log_action_triggered (); - if (m_logBook.fox_log ()->add_QSO (QSO_time, m_hisCall, m_hisGrid, m_rptSent, m_rptRcvd, m_lastBand)) - { - writeFoxQSO (QString {" Log: %1 %2 %3 %4 %5"}.arg (m_hisCall).arg (m_hisGrid) - .arg (m_rptSent).arg (m_rptRcvd).arg (m_lastBand)); - on_logQSOButton_clicked (); - m_foxRateQueue.enqueue (now); //Add present time in seconds - //to Rate queue. - QTimer::singleShot (13000, [=] { - m_foxQSOinProgress.removeOne(hc1); //Remove from In Progress window - updateFoxQSOsInProgressDisplay(); //Update InProgress display after Tx is complete - }); + auto already_logged = m_loggedByFox[hc1].contains(m_lastBand + " "); // already logged this call on this band? + + if (!already_logged) { // Log this QSO! + auto QSO_time = QDateTime::currentDateTimeUtc (); + m_hisCall=hc1; + m_hisGrid=m_foxQSO[hc1].grid; + m_rptSent=m_foxQSO[hc1].sent; + m_rptRcvd=m_foxQSO[hc1].rcvd; + if (!m_foxLogWindow) on_fox_log_action_triggered (); + if (m_logBook.fox_log ()->add_QSO (QSO_time, m_hisCall, m_hisGrid, m_rptSent, m_rptRcvd, m_lastBand)) + { + writeFoxQSO (QString {" Log: %1 %2 %3 %4 %5"}.arg (m_hisCall).arg (m_hisGrid) + .arg (m_rptSent).arg (m_rptRcvd).arg (m_lastBand)); + on_logQSOButton_clicked (); + m_foxRateQueue.enqueue (now); //Add present time in seconds + //to Rate queue. + QTimer::singleShot (13000, [=] { + m_foxQSOinProgress.removeOne(hc1); //Remove from In Progress window + updateFoxQSOsInProgressDisplay(); //Update InProgress display after Tx is complete + }); + } + m_loggedByFox[hc1] += (m_lastBand + " "); + } + else + { + // note that this is a duplicate + writeFoxQSO(QString{" Dup: %1 %2 %3 %4 %5"}.arg(m_hisCall).arg(m_hisGrid) + .arg(m_rptSent).arg(m_rptRcvd).arg(m_lastBand)); } - m_loggedByFox[hc1] += (m_lastBand + " "); } if(i