diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 490a0826..aea9678d 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -357,6 +357,11 @@ void MainWindow::on_actionImGuiDebug_triggered() p->imGuiDebugDialog_->show(); } +void MainWindow::on_actionDumpRadarProductRecords_triggered() +{ + manager::RadarProductManager::DumpRecords(); +} + void MainWindow::on_actionUserManual_triggered() { QDesktopServices::openUrl(QUrl {"https://supercell-wx.readthedocs.io/"}); @@ -392,6 +397,67 @@ void MainWindow::on_resourceTreeExpandAllButton_clicked() ui->resourceTreeView->expandAll(); } +void MainWindow::on_resourceTreeView_doubleClicked(const QModelIndex& index) +{ + std::string selectedString {index.data().toString().toStdString()}; + std::chrono::system_clock::time_point time {}; + + logger_->debug("Selecting resource: {}", + index.data().toString().toStdString()); + + static const std::string timeFormat {"%Y-%m-%d %H:%M:%S"}; + + std::istringstream in {selectedString}; + in >> std::chrono::parse(timeFormat, time); + + if (in.fail()) + { + // Not a time string, ignore double-click + return; + } + + QModelIndex parent1 = index.parent(); + QModelIndex parent2 = parent1.parent(); + QModelIndex parent3 = parent2.parent(); + + std::string radarSite {}; + std::string groupName {}; + std::string product {}; + + if (!parent2.isValid()) + { + // A time entry should be at the third or fourth level + logger_->error("Unexpected resource data"); + return; + } + + if (parent3.isValid()) + { + // Level 3 Product + radarSite = parent3.data().toString().toStdString(); + groupName = parent2.data().toString().toStdString(); + product = parent1.data().toString().toStdString(); + } + else + { + // Level 2 Product + radarSite = parent2.data().toString().toStdString(); + groupName = parent1.data().toString().toStdString(); + // No product index + } + + common::RadarProductGroup group = common::GetRadarProductGroup(groupName); + + // Update radar site if different from currently selected + if (p->activeMap_->GetRadarSite()->id() != radarSite) + { + p->activeMap_->SelectRadarSite(radarSite); + } + + // Select the updated radar product + p->activeMap_->SelectRadarProduct(group, product, 0, time); +} + void MainWindowImpl::ConfigureMapLayout() { auto& generalSettings = manager::SettingsManager::general_settings(); diff --git a/scwx-qt/source/scwx/qt/main/main_window.hpp b/scwx-qt/source/scwx/qt/main/main_window.hpp index 0902c0e6..f67374fe 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.hpp +++ b/scwx-qt/source/scwx/qt/main/main_window.hpp @@ -37,6 +37,7 @@ private slots: void on_actionSettings_triggered(); void on_actionExit_triggered(); void on_actionImGuiDebug_triggered(); + void on_actionDumpRadarProductRecords_triggered(); void on_actionUserManual_triggered(); void on_actionDiscord_triggered(); void on_actionGitHubRepository_triggered(); @@ -44,6 +45,7 @@ private slots: void on_radarSiteSelectButton_clicked(); void on_resourceTreeCollapseAllButton_clicked(); void on_resourceTreeExpandAllButton_clicked(); + void on_resourceTreeView_doubleClicked(const QModelIndex& index); private: std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/main/main_window.ui b/scwx-qt/source/scwx/qt/main/main_window.ui index 7cf48f3c..77d45c5e 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.ui +++ b/scwx-qt/source/scwx/qt/main/main_window.ui @@ -83,6 +83,8 @@ &Debug + + @@ -397,6 +399,11 @@ &GitHub Repository + + + Dump Radar &Product Records + + diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 80514e49..293e69d5 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -16,6 +16,7 @@ #pragma warning(push, 0) #include +#include #include #include #include @@ -36,8 +37,10 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); typedef std::function()> CreateNexradFileFunction; typedef std::map> + std::weak_ptr> RadarProductRecordMap; +typedef std::list> + RadarProductRecordList; static constexpr uint32_t NUM_RADIAL_GATES_0_5_DEGREE = common::MAX_0_5_DEGREE_RADIALS * common::MAX_DATA_MOMENT_GATES; @@ -53,8 +56,8 @@ static const std::string kDefaultLevel3Product_ {"N0B"}; static constexpr std::chrono::seconds kRetryInterval_ {15}; static std::unordered_map> - instanceMap_; -static std::mutex instanceMutex_; + instanceMap_; +static std::shared_mutex instanceMutex_; static std::unordered_map> @@ -123,7 +126,9 @@ public: coordinates0_5Degree_ {}, coordinates1Degree_ {}, level2ProductRecords_ {}, + level2ProductRecentRecords_ {}, level3ProductRecordsMap_ {}, + level3ProductRecentRecordsMap_ {}, level2ProductRecordMutex_ {}, level3ProductRecordMutex_ {}, level2ProviderManager_ {std::make_shared( @@ -159,6 +164,10 @@ public: auto& [key, providerManager] = p; providerManager->Disable(); }); + + // Lock other mutexes before destroying, ensure loading is complete + std::unique_lock loadLevel2DataLock {loadLevel2DataMutex_}; + std::unique_lock loadLevel3DataLock {loadLevel3DataMutex_}; } RadarProductManager* self_; @@ -166,17 +175,22 @@ public: std::shared_ptr GetLevel3ProviderManager(const std::string& product); - void EnableRefresh(std::shared_ptr providerManager, + void EnableRefresh(boost::uuids::uuid uuid, + std::shared_ptr providerManager, bool enabled); void RefreshData(std::shared_ptr providerManager); - std::shared_ptr + std::tuple, + std::chrono::system_clock::time_point> GetLevel2ProductRecord(std::chrono::system_clock::time_point time); - std::shared_ptr + std::tuple, + std::chrono::system_clock::time_point> GetLevel3ProductRecord(const std::string& product, std::chrono::system_clock::time_point time); std::shared_ptr StoreRadarProductRecord(std::shared_ptr record); + void UpdateRecentRecords(RadarProductRecordList& recentList, + std::shared_ptr record); void LoadProviderData(std::chrono::system_clock::time_point time, std::shared_ptr providerManager, @@ -199,10 +213,12 @@ public: std::vector coordinates0_5Degree_; std::vector coordinates1Degree_; - RadarProductRecordMap level2ProductRecords_; + RadarProductRecordMap level2ProductRecords_; + RadarProductRecordList level2ProductRecentRecords_; std::unordered_map level3ProductRecordsMap_; - + std::unordered_map + level3ProductRecentRecordsMap_; std::shared_mutex level2ProductRecordMutex_; std::shared_mutex level3ProductRecordMutex_; @@ -218,6 +234,12 @@ public: common::Level3ProductCategoryMap availableCategoryMap_; std::shared_mutex availableCategoryMutex_; + + std::unordered_map, + boost::hash> + refreshMap_ {}; + std::mutex refreshMapMutex_ {}; }; RadarProductManager::RadarProductManager(const std::string& radarId) : @@ -248,6 +270,8 @@ std::string ProviderManager::name() const void ProviderManager::Disable() { + logger_->debug("Disabling refresh: {}", name()); + std::unique_lock lock(refreshTimerMutex_); refreshEnabled_ = false; refreshTimer_.cancel(); @@ -266,6 +290,61 @@ void RadarProductManager::Cleanup() } } +void RadarProductManager::DumpRecords() +{ + scwx::util::async( + [] + { + logger_->info("Record Dump"); + + std::shared_lock instanceLock {instanceMutex_}; + for (auto& instance : instanceMap_) + { + auto radarProductManager = instance.second.lock(); + if (radarProductManager != nullptr) + { + logger_->info(" {}", radarProductManager->radar_site()->id()); + logger_->info(" Level 2"); + + { + std::shared_lock level2ProductLock { + radarProductManager->p->level2ProductRecordMutex_}; + + for (auto& record : + radarProductManager->p->level2ProductRecords_) + { + logger_->info(" {}{}", + scwx::util::TimeString(record.first), + record.second.expired() ? " (expired)" : ""); + } + } + + logger_->info(" Level 3"); + + { + std::shared_lock level3ProductLock { + radarProductManager->p->level3ProductRecordMutex_}; + + for (auto& recordMap : + radarProductManager->p->level3ProductRecordsMap_) + { + // Product Name + logger_->info(" {}", recordMap.first); + + for (auto& record : recordMap.second) + { + logger_->info(" {}{}", + scwx::util::TimeString(record.first), + record.second.expired() ? " (expired)" : + ""); + } + } + } + } + } + }); +} + const std::vector& RadarProductManager::coordinates(common::RadialSize radialSize) const { @@ -413,11 +492,12 @@ RadarProductManagerImpl::GetLevel3ProviderManager(const std::string& product) void RadarProductManager::EnableRefresh(common::RadarProductGroup group, const std::string& product, - bool enabled) + bool enabled, + boost::uuids::uuid uuid) { if (group == common::RadarProductGroup::Level2) { - p->EnableRefresh(p->level2ProviderManager_, enabled); + p->EnableRefresh(uuid, p->level2ProviderManager_, enabled); } else { @@ -437,16 +517,65 @@ void RadarProductManager::EnableRefresh(common::RadarProductGroup group, availableProducts.cend(), product) != availableProducts.cend()) { - p->EnableRefresh(providerManager, enabled); + p->EnableRefresh(uuid, providerManager, enabled); } }); } } void RadarProductManagerImpl::EnableRefresh( - std::shared_ptr providerManager, bool enabled) + boost::uuids::uuid uuid, + std::shared_ptr providerManager, + bool enabled) { - if (providerManager->refreshEnabled_ != enabled) + // Lock the refresh map + std::unique_lock lock {refreshMapMutex_}; + + auto currentProviderManager = refreshMap_.find(uuid); + if (currentProviderManager != refreshMap_.cend()) + { + // If the enabling refresh for a different product, or disabling refresh + if (currentProviderManager->second != providerManager || !enabled) + { + // Determine number of entries in the map for the current provider + // manager + auto currentProviderManagerCount = std::count_if( + refreshMap_.cbegin(), + refreshMap_.cend(), + [&](const auto& provider) + { return provider.second == currentProviderManager->second; }); + + // If this is the last reference to the provider in the refresh map + if (currentProviderManagerCount == 1) + { + // Disable current provider + currentProviderManager->second->Disable(); + } + + // Dissociate uuid from current provider manager + refreshMap_.erase(currentProviderManager); + + // If we are enabling a new provider manager + if (enabled) + { + // Associate uuid to providerManager + refreshMap_.emplace(uuid, providerManager); + } + } + } + else if (enabled) + { + // We are enabling a new provider manager + // Associate uuid to provider manager + refreshMap_.emplace(uuid, providerManager); + } + + // Release the refresh map mutex + lock.unlock(); + + // We have already handled a disable request by this point. If enabling, and + // the provider manager refresh isn't already enabled, enable it. + if (enabled && providerManager->refreshEnabled_ != enabled) { providerManager->refreshEnabled_ = enabled; @@ -475,7 +604,7 @@ void RadarProductManagerImpl::RefreshData( std::chrono::milliseconds interval = kRetryInterval_; - if (newObjects > 0) + if (totalObjects > 0) { std::string key = providerManager->provider_->FindLatestKey(); auto latestTime = @@ -491,10 +620,14 @@ void RadarProductManagerImpl::RefreshData( interval = kRetryInterval_; } - emit providerManager->NewDataAvailable( - providerManager->group_, providerManager->product_, latestTime); + if (newObjects > 0) + { + emit providerManager->NewDataAvailable(providerManager->group_, + providerManager->product_, + latestTime); + } } - else if (providerManager->refreshEnabled_ && totalObjects == 0) + else if (providerManager->refreshEnabled_) { logger_->info("[{}] No data found, disabling refresh", providerManager->name()); @@ -562,10 +695,13 @@ void RadarProductManagerImpl::LoadProviderData( auto it = recordMap.find(time); if (it != recordMap.cend()) { - logger_->debug( - "Data previously loaded, loading from data cache"); + existingRecord = it->second.lock(); - existingRecord = it->second; + if (existingRecord != nullptr) + { + logger_->debug( + "Data previously loaded, loading from data cache"); + } } } @@ -727,38 +863,70 @@ void RadarProductManagerImpl::LoadNexradFile( }); } -std::shared_ptr +std::tuple, + std::chrono::system_clock::time_point> RadarProductManagerImpl::GetLevel2ProductRecord( std::chrono::system_clock::time_point time) { - std::shared_ptr record; + std::shared_ptr record {nullptr}; + RadarProductRecordMap::const_pointer recordPtr {nullptr}; + std::chrono::system_clock::time_point recordTime {time}; if (!level2ProductRecords_.empty() && time == std::chrono::system_clock::time_point {}) { // If a default-initialized time point is given, return the latest record - record = level2ProductRecords_.rbegin()->second; + recordPtr = &(*level2ProductRecords_.rbegin()); } else { - // TODO: Round to minutes - record = scwx::util::GetBoundedElementValue(level2ProductRecords_, time); + recordPtr = + scwx::util::GetBoundedElementPointer(level2ProductRecords_, time); + } - // Does the record contain the time we are looking for? - if (record != nullptr && (time < record->level2_file()->start_time())) + if (recordPtr != nullptr) + { + if (time == std::chrono::system_clock::time_point {} || + time == recordPtr->first) { - record = nullptr; + recordTime = recordPtr->first; + record = recordPtr->second.lock(); } } - return record; + if (record == nullptr && + recordTime != std::chrono::system_clock::time_point {}) + { + // Product is expired, reload it + std::shared_ptr request = + std::make_shared(); + + QObject::connect( + request.get(), + &request::NexradFileRequest::RequestComplete, + self_, + [this](std::shared_ptr request) + { + if (request->radar_product_record() != nullptr) + { + emit self_->DataReloaded(request->radar_product_record()); + } + }); + + self_->LoadLevel2Data(recordTime, request); + } + + return {record, recordTime}; } -std::shared_ptr +std::tuple, + std::chrono::system_clock::time_point> RadarProductManagerImpl::GetLevel3ProductRecord( const std::string& product, std::chrono::system_clock::time_point time) { - std::shared_ptr record = nullptr; + std::shared_ptr record {nullptr}; + RadarProductRecordMap::const_pointer recordPtr {nullptr}; + std::chrono::system_clock::time_point recordTime {time}; std::unique_lock lock {level3ProductRecordMutex_}; @@ -770,15 +938,50 @@ RadarProductManagerImpl::GetLevel3ProductRecord( { // If a default-initialized time point is given, return the latest // record - record = it->second.rbegin()->second; + recordPtr = &(*it->second.rbegin()); } else { - record = scwx::util::GetBoundedElementValue(it->second, time); + recordPtr = scwx::util::GetBoundedElementPointer(it->second, time); } } - return record; + // Lock is no longer needed + lock.unlock(); + + if (recordPtr != nullptr) + { + if (time == std::chrono::system_clock::time_point {} || + time == recordPtr->first) + { + recordTime = recordPtr->first; + record = recordPtr->second.lock(); + } + } + + if (record == nullptr && + recordTime != std::chrono::system_clock::time_point {}) + { + // Product is expired, reload it + std::shared_ptr request = + std::make_shared(); + + QObject::connect( + request.get(), + &request::NexradFileRequest::RequestComplete, + self_, + [this](std::shared_ptr request) + { + if (request->radar_product_record() != nullptr) + { + emit self_->DataReloaded(request->radar_product_record()); + } + }); + + self_->LoadLevel3Data(product, recordTime, request); + } + + return {record, recordTime}; } std::shared_ptr @@ -787,7 +990,7 @@ RadarProductManagerImpl::StoreRadarProductRecord( { logger_->debug("StoreRadarProductRecord()"); - std::shared_ptr storedRecord = record; + std::shared_ptr storedRecord = nullptr; auto timeInSeconds = std::chrono::time_point_castdebug( - "Level 2 product previously loaded, loading from cache"); + storedRecord = it->second.lock(); - storedRecord = it->second; + if (storedRecord != nullptr) + { + logger_->debug( + "Level 2 product previously loaded, loading from cache"); + } } - else + + if (storedRecord == nullptr) { + storedRecord = record; level2ProductRecords_[timeInSeconds] = record; } + + UpdateRecentRecords(level2ProductRecentRecords_, storedRecord); } else if (record->radar_product_group() == common::RadarProductGroup::Level3) { @@ -819,23 +1029,58 @@ RadarProductManagerImpl::StoreRadarProductRecord( auto it = productMap.find(timeInSeconds); if (it != productMap.cend()) { - logger_->debug( - "Level 3 product previously loaded, loading from cache"); + storedRecord = it->second.lock(); - storedRecord = it->second; + if (storedRecord != nullptr) + { + logger_->debug( + "Level 3 product previously loaded, loading from cache"); + } } - else + + if (storedRecord == nullptr) { + storedRecord = record; productMap[timeInSeconds] = record; } + + UpdateRecentRecords( + level3ProductRecentRecordsMap_[record->radar_product()], storedRecord); } return storedRecord; } +void RadarProductManagerImpl::UpdateRecentRecords( + RadarProductRecordList& recentList, + std::shared_ptr record) +{ + static constexpr std::size_t kRecentListMaxSize_ {2u}; + + auto it = std::find(recentList.cbegin(), recentList.cend(), record); + if (it != recentList.cbegin() && it != recentList.cend()) + { + // If the record exists beyond the front of the list, remove it + recentList.erase(it); + } + + if (recentList.size() == 0 || it != recentList.cbegin()) + { + // Add the record to the front of the list, unless it's already there + recentList.push_front(record); + } + + while (recentList.size() > kRecentListMaxSize_) + { + // Remove from the end of the list while it's too big + recentList.pop_back(); + } +} + std::tuple, float, - std::vector> + std::vector, + std::chrono::system_clock::time_point> RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, float elevation, std::chrono::system_clock::time_point time) @@ -844,8 +1089,8 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, float elevationCut = 0.0f; std::vector elevationCuts; - std::shared_ptr record = - p->GetLevel2ProductRecord(time); + std::shared_ptr record; + std::tie(record, time) = p->GetLevel2ProductRecord(time); if (record != nullptr) { @@ -854,24 +1099,25 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, dataBlockType, elevation, time); } - return std::tie(radarData, elevationCut, elevationCuts); + return {radarData, elevationCut, elevationCuts, time}; } -std::shared_ptr +std::tuple, + std::chrono::system_clock::time_point> RadarProductManager::GetLevel3Data(const std::string& product, std::chrono::system_clock::time_point time) { std::shared_ptr message = nullptr; - std::shared_ptr record = - p->GetLevel3ProductRecord(product, time); + std::shared_ptr record; + std::tie(record, time) = p->GetLevel3ProductRecord(product, time); if (record != nullptr) { message = record->level3_file()->message(); } - return message; + return {message, time}; } common::Level3ProductCategoryMap @@ -970,7 +1216,7 @@ RadarProductManager::Instance(const std::string& radarSite) bool instanceCreated = false; { - std::lock_guard guard(instanceMutex_); + std::unique_lock lock {instanceMutex_}; // Look up instance weak pointer auto it = instanceMap_.find(radarSite); diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp index 80a3cca7..54a57499 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp @@ -12,6 +12,7 @@ #include #include +#include #include namespace scwx @@ -33,23 +34,63 @@ public: static void Cleanup(); + /** + * @brief Debug function to dump currently loaded products to the log. + */ + static void DumpRecords(); + const std::vector& coordinates(common::RadialSize radialSize) const; float gate_size() const; std::shared_ptr radar_site() const; void Initialize(); + + /** + * @brief Enables or disables refresh associated with a unique identifier + * (UUID) for a given radar product group and product. + * + * Only a single product refresh can be enabled for a given UUID. If a second + * product refresh is enabled for the same UUID, the first product refresh is + * disabled (unless still enabled under a different UUID). + * + * @param [in] group Radar product group + * @param [in] product Radar product name + * @param [in] enabled Whether to enable refresh + * @param [in] uuid Unique identifier. Default is boost::uuids::nil_uuid(). + */ void EnableRefresh(common::RadarProductGroup group, const std::string& product, - bool enabled); + bool enabled, + boost::uuids::uuid uuid = boost::uuids::nil_uuid()); + /** + * @brief Get level 2 radar data for a data block type, elevation, and time. + * + * @param [in] dataBlockType Data block type + * @param [in] elevation Elevation tilt + * @param [in] time Radar product time + * + * @return Level 2 radar data, selected elevation cut, available elevation + * cuts and selected time + */ std::tuple, float, - std::vector> + std::vector, + std::chrono::system_clock::time_point> GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, float elevation, std::chrono::system_clock::time_point time = {}); - std::shared_ptr + /** + * @brief Get level 3 message data for a product and time. + * + * @param [in] product Radar product name + * @param [in] time Radar product time + * + * @return Level 3 message data and selected time + */ + std::tuple, + std::chrono::system_clock::time_point> GetLevel3Data(const std::string& product, std::chrono::system_clock::time_point time = {}); @@ -76,6 +117,7 @@ public: void UpdateAvailableProducts(); signals: + void DataReloaded(std::shared_ptr record); void Level3ProductsChanged(); void NewDataAvailable(common::RadarProductGroup group, const std::string& product, diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 40d45910..0b4d56ba 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -58,6 +59,7 @@ class MapWidgetImpl : public QObject public: explicit MapWidgetImpl(MapWidget* widget, const QMapLibreGL::Settings& settings) : + uuid_ {boost::uuids::random_generator()()}, context_ {std::make_shared()}, widget_ {widget}, settings_(settings), @@ -126,6 +128,8 @@ public: common::Level2Product GetLevel2ProductOrDefault(const std::string& productName) const; + boost::uuids::uuid uuid_; + std::shared_ptr context_; MapWidget* widget_; @@ -303,7 +307,7 @@ std::shared_ptr MapWidget::GetRadarSite() const return radarSite; } -uint16_t MapWidget::GetVcp() const +std::uint16_t MapWidget::GetVcp() const { auto radarProductView = p->context_->radar_product_view(); @@ -313,7 +317,7 @@ uint16_t MapWidget::GetVcp() const } else { - return 0; + return 0u; } } @@ -330,7 +334,8 @@ void MapWidget::SelectElevation(float elevation) void MapWidget::SelectRadarProduct(common::RadarProductGroup group, const std::string& product, - int16_t productCode) + std::int16_t productCode, + std::chrono::system_clock::time_point time) { bool radarProductViewCreated = false; @@ -380,8 +385,8 @@ void MapWidget::SelectRadarProduct(common::RadarProductGroup group, if (radarProductView != nullptr) { - // Always select the latest product available - radarProductView->SelectTime({}); + // Select the time associated with the request + radarProductView->SelectTime(time); if (radarProductViewCreated) { @@ -399,7 +404,8 @@ void MapWidget::SelectRadarProduct(common::RadarProductGroup group, if (p->autoRefreshEnabled_) { - p->radarProductManager_->EnableRefresh(group, productName, true); + p->radarProductManager_->EnableRefresh( + group, productName, true, p->uuid_); } } @@ -485,7 +491,8 @@ void MapWidget::SetAutoRefresh(bool enabled) p->radarProductManager_->EnableRefresh( radarProductView->GetRadarProductGroup(), radarProductView->GetRadarProductName(), - true); + true, + p->uuid_); } } } diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index 63f51160..104c280f 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -41,12 +41,23 @@ public: common::RadarProductGroup GetRadarProductGroup() const; std::string GetRadarProductName() const; std::shared_ptr GetRadarSite() const; - uint16_t GetVcp() const; + std::uint16_t GetVcp() const; void SelectElevation(float elevation); + + /** + * @brief Selects a radar product. + * + * @param [in] group Radar product group + * @param [in] product Radar product name + * @param [in] productCode Radar product code (optional) + * @paran [in] time Product time. Default is the latest available. + */ void SelectRadarProduct(common::RadarProductGroup group, const std::string& product, - int16_t productCode); + std::int16_t productCode = 0, + std::chrono::system_clock::time_point time = {}); + void SelectRadarProduct(std::shared_ptr record); /** diff --git a/scwx-qt/source/scwx/qt/model/radar_product_model.cpp b/scwx-qt/source/scwx/qt/model/radar_product_model.cpp index 3608f49f..510d41ef 100644 --- a/scwx-qt/source/scwx/qt/model/radar_product_model.cpp +++ b/scwx-qt/source/scwx/qt/model/radar_product_model.cpp @@ -105,10 +105,16 @@ RadarProductModelImpl::RadarProductModelImpl(RadarProductModel* self) : } } - // Create leaf item for product time - model_->AppendRow(productItem, - new TreeItem {QString::fromStdString( - util::TimeString(latestTime))}); + // Find existing time item (e.g., 2023-04-10 10:11:12) + const QString timeString = + QString::fromStdString(util::TimeString(latestTime)); + TreeItem* timeItem = productItem->FindChild(0, timeString); + + if (timeItem == nullptr) + { + // Create leaf item for product time + model_->AppendRow(productItem, new TreeItem {timeString}); + } }, Qt::QueuedConnection); }); diff --git a/scwx-qt/source/scwx/qt/ui/level2_products_widget.cpp b/scwx-qt/source/scwx/qt/ui/level2_products_widget.cpp index 31a3750e..dc6d80f5 100644 --- a/scwx-qt/source/scwx/qt/ui/level2_products_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level2_products_widget.cpp @@ -119,8 +119,7 @@ void Level2ProductsWidgetImpl::UpdateProductSelection( { const std::string& productName = common::GetLevel2Name(product); - std::for_each(std::execution::par_unseq, - productButtons_.cbegin(), + std::for_each(productButtons_.cbegin(), productButtons_.cend(), [&](auto& toolButton) { diff --git a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp index ed582073..71f0946b 100644 --- a/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level2_settings_widget.cpp @@ -51,7 +51,6 @@ public: void NormalizeElevationButtons(); void SelectElevation(float elevation); - void UpdateSettings(); Level2SettingsWidget* self_; QLayout* layout_; @@ -135,8 +134,7 @@ void Level2SettingsWidget::UpdateElevationSelection(float elevation) QString buttonText {QString::number(elevation, 'f', 1) + common::Characters::DEGREE}; - std::for_each(std::execution::par_unseq, - p->elevationButtons_.cbegin(), + std::for_each(p->elevationButtons_.cbegin(), p->elevationButtons_.cend(), [&](auto& toolButton) { diff --git a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp index fe5b9bdf..8c71f7b3 100644 --- a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp @@ -258,8 +258,7 @@ void Level3ProductsWidgetImpl::UpdateCategorySelection( { const std::string& categoryName = common::GetLevel3CategoryName(category); - std::for_each(std::execution::par_unseq, - categoryButtons_.cbegin(), + std::for_each(categoryButtons_.cbegin(), categoryButtons_.cend(), [&](auto& toolButton) { diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp index 73b3346a..f1f501db 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -44,7 +44,6 @@ public: explicit Level2ProductViewImpl(common::Level2Product product) : product_ {product}, selectedElevation_ {0.0f}, - selectedTime_ {}, elevationScan_ {nullptr}, momentDataBlock0_ {nullptr}, latitude_ {}, @@ -72,8 +71,7 @@ public: common::Level2Product product_; wsr88d::rda::DataBlockType dataBlockType_; - float selectedElevation_; - std::chrono::system_clock::time_point selectedTime_; + float selectedElevation_; std::shared_ptr elevationScan_; std::shared_ptr momentDataBlock0_; @@ -108,9 +106,36 @@ Level2ProductView::Level2ProductView( RadarProductView(radarProductManager), p(std::make_unique(product)) { + ConnectRadarProductManager(); } Level2ProductView::~Level2ProductView() = default; +void Level2ProductView::ConnectRadarProductManager() +{ + connect(radar_product_manager().get(), + &manager::RadarProductManager::DataReloaded, + this, + [this](std::shared_ptr record) + { + if (record->radar_product_group() == + common::RadarProductGroup::Level2 && + record->time() == selected_time()) + { + // If the data associated with the currently selected time is + // reloaded, update the view + Update(); + } + }); +} + +void Level2ProductView::DisconnectRadarProductManager() +{ + disconnect(radar_product_manager().get(), + &manager::RadarProductManager::DataReloaded, + this, + nullptr); +} + const std::vector& Level2ProductView::color_table() const { @@ -243,11 +268,6 @@ void Level2ProductView::SelectProduct(const std::string& productName) p->SetProduct(productName); } -void Level2ProductView::SelectTime(std::chrono::system_clock::time_point time) -{ - p->selectedTime_ = time; -} - void Level2ProductViewImpl::SetProduct(const std::string& productName) { SetProduct(common::GetLevel2Product(productName)); @@ -376,9 +396,18 @@ void Level2ProductView::ComputeSweep() radar_product_manager(); std::shared_ptr radarData; - std::tie(radarData, p->elevationCut_, p->elevationCuts_) = + std::chrono::system_clock::time_point requestedTime {selected_time()}; + std::chrono::system_clock::time_point foundTime; + std::tie(radarData, p->elevationCut_, p->elevationCuts_, foundTime) = radarProductManager->GetLevel2Data( - p->dataBlockType_, p->selectedElevation_, p->selectedTime_); + p->dataBlockType_, p->selectedElevation_, requestedTime); + + // If a different time was found than what was requested, update it + if (requestedTime != foundTime) + { + SelectTime(foundTime); + } + if (radarData == nullptr || radarData == p->elevationScan_) { return; diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.hpp b/scwx-qt/source/scwx/qt/view/level2_product_view.hpp index 8043a0ab..a00dd0da 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.hpp @@ -28,31 +28,34 @@ public: ~Level2ProductView(); const std::vector& color_table() const override; - uint16_t color_table_min() const override; - uint16_t color_table_max() const override; + std::uint16_t color_table_min() const override; + std::uint16_t color_table_max() const override; float elevation() const override; float range() const override; std::chrono::system_clock::time_point sweep_time() const override; - uint16_t vcp() const override; + std::uint16_t vcp() const override; const std::vector& vertices() const override; void LoadColorTable(std::shared_ptr colorTable) override; void SelectElevation(float elevation) override; void SelectProduct(const std::string& productName) override; - void SelectTime(std::chrono::system_clock::time_point time) override; void Update() override; common::RadarProductGroup GetRadarProductGroup() const override; std::string GetRadarProductName() const override; std::vector GetElevationCuts() const override; - std::tuple GetMomentData() const override; - std::tuple GetCfpMomentData() const override; + std::tuple + GetMomentData() const override; + std::tuple + GetCfpMomentData() const override; static std::shared_ptr Create(common::Level2Product product, std::shared_ptr radarProductManager); protected: + void ConnectRadarProductManager() override; + void DisconnectRadarProductManager() override; void UpdateColorTable() override; protected slots: diff --git a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp index 13b172e4..a0cc3ea5 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp @@ -59,9 +59,37 @@ Level3ProductView::Level3ProductView( RadarProductView(radarProductManager), p(std::make_unique(product)) { + ConnectRadarProductManager(); } Level3ProductView::~Level3ProductView() = default; +void Level3ProductView::ConnectRadarProductManager() +{ + connect(radar_product_manager().get(), + &manager::RadarProductManager::DataReloaded, + this, + [this](std::shared_ptr record) + { + if (record->radar_product_group() == + common::RadarProductGroup::Level3 && + record->radar_product() == p->product_ && + record->time() == selected_time()) + { + // If the data associated with the currently selected time is + // reloaded, update the view + Update(); + } + }); +} + +void Level3ProductView::DisconnectRadarProductManager() +{ + disconnect(radar_product_manager().get(), + &manager::RadarProductManager::DataReloaded, + this, + nullptr); +} + const std::vector& Level3ProductView::color_table() const { diff --git a/scwx-qt/source/scwx/qt/view/level3_product_view.hpp b/scwx-qt/source/scwx/qt/view/level3_product_view.hpp index 267d8678..c9d299eb 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.hpp @@ -28,8 +28,8 @@ public: virtual ~Level3ProductView(); const std::vector& color_table() const override; - uint16_t color_table_min() const override; - uint16_t color_table_max() const override; + std::uint16_t color_table_min() const override; + std::uint16_t color_table_max() const override; void LoadColorTable(std::shared_ptr colorTable) override; void Update() override; @@ -45,6 +45,8 @@ protected: void set_graphic_product_message( std::shared_ptr gpm); + void ConnectRadarProductManager() override; + void DisconnectRadarProductManager() override; void UpdateColorTable() override; private: diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp index 1731ab3d..4652c0a2 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -27,25 +27,18 @@ class Level3RadialViewImpl { public: explicit Level3RadialViewImpl() : - selectedTime_ {}, - latitude_ {}, - longitude_ {}, - range_ {}, - vcp_ {}, - sweepTime_ {} + latitude_ {}, longitude_ {}, range_ {}, vcp_ {}, sweepTime_ {} { } ~Level3RadialViewImpl() = default; - std::chrono::system_clock::time_point selectedTime_; + std::vector vertices_; + std::vector dataMoments8_; - std::vector vertices_; - std::vector dataMoments8_; - - float latitude_; - float longitude_; - float range_; - uint16_t vcp_; + float latitude_; + float longitude_; + float range_; + std::uint16_t vcp_; std::chrono::system_clock::time_point sweepTime_; }; @@ -92,11 +85,6 @@ std::tuple Level3RadialView::GetMomentData() const return std::tie(data, dataSize, componentSize); } -void Level3RadialView::SelectTime(std::chrono::system_clock::time_point time) -{ - p->selectedTime_ = time; -} - void Level3RadialView::ComputeSweep() { logger_->debug("ComputeSweep()"); @@ -109,9 +97,18 @@ void Level3RadialView::ComputeSweep() radar_product_manager(); // Retrieve message from Radar Product Manager - std::shared_ptr message = - radarProductManager->GetLevel3Data(GetRadarProductName(), - p->selectedTime_); + std::shared_ptr message; + std::chrono::system_clock::time_point requestedTime {selected_time()}; + std::chrono::system_clock::time_point foundTime; + std::tie(message, foundTime) = + radarProductManager->GetLevel3Data(GetRadarProductName(), requestedTime); + + // If a different time was found than what was requested, update it + if (requestedTime != foundTime) + { + SelectTime(foundTime); + } + if (message == nullptr) { logger_->debug("Level 3 data not found"); diff --git a/scwx-qt/source/scwx/qt/view/level3_radial_view.hpp b/scwx-qt/source/scwx/qt/view/level3_radial_view.hpp index ce034df2..a971941e 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.hpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.hpp @@ -27,12 +27,11 @@ public: float range() const override; std::chrono::system_clock::time_point sweep_time() const override; - uint16_t vcp() const override; + std::uint16_t vcp() const override; const std::vector& vertices() const override; - void SelectTime(std::chrono::system_clock::time_point time) override; - - std::tuple GetMomentData() const override; + std::tuple + GetMomentData() const override; static std::shared_ptr Create(const std::string& product, diff --git a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp index 84c896ae..1c150c70 100644 --- a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp @@ -27,18 +27,11 @@ class Level3RasterViewImpl { public: explicit Level3RasterViewImpl() : - selectedTime_ {}, - latitude_ {}, - longitude_ {}, - range_ {}, - vcp_ {}, - sweepTime_ {} + latitude_ {}, longitude_ {}, range_ {}, vcp_ {}, sweepTime_ {} { } ~Level3RasterViewImpl() = default; - std::chrono::system_clock::time_point selectedTime_; - std::vector vertices_; std::vector dataMoments8_; @@ -92,11 +85,6 @@ std::tuple Level3RasterView::GetMomentData() const return std::tie(data, dataSize, componentSize); } -void Level3RasterView::SelectTime(std::chrono::system_clock::time_point time) -{ - p->selectedTime_ = time; -} - void Level3RasterView::ComputeSweep() { logger_->debug("ComputeSweep()"); @@ -109,9 +97,18 @@ void Level3RasterView::ComputeSweep() radar_product_manager(); // Retrieve message from Radar Product Manager - std::shared_ptr message = - radarProductManager->GetLevel3Data(GetRadarProductName(), - p->selectedTime_); + std::shared_ptr message; + std::chrono::system_clock::time_point requestedTime {selected_time()}; + std::chrono::system_clock::time_point foundTime; + std::tie(message, foundTime) = + radarProductManager->GetLevel3Data(GetRadarProductName(), requestedTime); + + // If a different time was found than what was requested, update it + if (requestedTime != foundTime) + { + SelectTime(foundTime); + } + if (message == nullptr) { logger_->debug("Level 3 data not found"); diff --git a/scwx-qt/source/scwx/qt/view/level3_raster_view.hpp b/scwx-qt/source/scwx/qt/view/level3_raster_view.hpp index a3f0a973..6b852f32 100644 --- a/scwx-qt/source/scwx/qt/view/level3_raster_view.hpp +++ b/scwx-qt/source/scwx/qt/view/level3_raster_view.hpp @@ -27,12 +27,11 @@ public: float range() const override; std::chrono::system_clock::time_point sweep_time() const override; - uint16_t vcp() const override; + std::uint16_t vcp() const override; const std::vector& vertices() const override; - void SelectTime(std::chrono::system_clock::time_point time) override; - - std::tuple GetMomentData() const override; + std::tuple + GetMomentData() const override; static std::shared_ptr Create(const std::string& product, diff --git a/scwx-qt/source/scwx/qt/view/radar_product_view.cpp b/scwx-qt/source/scwx/qt/view/radar_product_view.cpp index 8412ba3a..99cd78c6 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.cpp @@ -16,12 +16,12 @@ static const std::string logPrefix_ = "scwx::qt::view::radar_product_view"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); // Default color table should be transparent to prevent flicker -static const std::vector DEFAULT_COLOR_TABLE = { +static const std::vector kDefaultColorTable_ = { boost::gil::rgba8_pixel_t(0, 128, 0, 0), boost::gil::rgba8_pixel_t(255, 192, 0, 0), boost::gil::rgba8_pixel_t(255, 0, 0, 0)}; -static const uint16_t DEFAULT_COLOR_TABLE_MIN = 2u; -static const uint16_t DEFAULT_COLOR_TABLE_MAX = 255u; +static const std::uint16_t kDefaultColorTableMin_ = 2u; +static const std::uint16_t kDefaultColorTableMax_ = 255u; class RadarProductViewImpl { @@ -30,6 +30,7 @@ public: std::shared_ptr radarProductManager) : initialized_ {false}, sweepMutex_ {}, + selectedTime_ {}, radarProductManager_ {radarProductManager} { } @@ -38,6 +39,8 @@ public: bool initialized_; std::mutex sweepMutex_; + std::chrono::system_clock::time_point selectedTime_; + std::shared_ptr radarProductManager_; }; @@ -49,17 +52,17 @@ RadarProductView::~RadarProductView() = default; const std::vector& RadarProductView::color_table() const { - return DEFAULT_COLOR_TABLE; + return kDefaultColorTable_; } -uint16_t RadarProductView::color_table_min() const +std::uint16_t RadarProductView::color_table_min() const { - return DEFAULT_COLOR_TABLE_MIN; + return kDefaultColorTableMin_; } -uint16_t RadarProductView::color_table_max() const +std::uint16_t RadarProductView::color_table_max() const { - return DEFAULT_COLOR_TABLE_MAX; + return kDefaultColorTableMax_; } float RadarProductView::elevation() const @@ -78,6 +81,11 @@ float RadarProductView::range() const return 0.0f; } +std::chrono::system_clock::time_point RadarProductView::selected_time() const +{ + return p->selectedTime_; +} + std::chrono::system_clock::time_point RadarProductView::sweep_time() const { return {}; @@ -91,7 +99,9 @@ std::mutex& RadarProductView::sweep_mutex() void RadarProductView::set_radar_product_manager( std::shared_ptr radarProductManager) { + DisconnectRadarProductManager(); p->radarProductManager_ = radarProductManager; + ConnectRadarProductManager(); } void RadarProductView::Initialize() @@ -103,6 +113,11 @@ void RadarProductView::Initialize() void RadarProductView::SelectElevation(float /*elevation*/) {} +void RadarProductView::SelectTime(std::chrono::system_clock::time_point time) +{ + p->selectedTime_ = time; +} + bool RadarProductView::IsInitialized() const { return p->initialized_; @@ -113,12 +128,12 @@ std::vector RadarProductView::GetElevationCuts() const return {}; } -std::tuple +std::tuple RadarProductView::GetCfpMomentData() const { const void* data = nullptr; - size_t dataSize = 0; - size_t componentSize = 0; + std::size_t dataSize = 0; + std::size_t componentSize = 0; return std::tie(data, dataSize, componentSize); } diff --git a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp index 51b3334c..04c9131d 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp @@ -30,15 +30,16 @@ public: virtual ~RadarProductView(); virtual const std::vector& color_table() const; - virtual uint16_t color_table_min() const; - virtual uint16_t color_table_max() const; + virtual std::uint16_t color_table_min() const; + virtual std::uint16_t color_table_max() const; virtual float elevation() const; virtual float range() const; virtual std::chrono::system_clock::time_point sweep_time() const; - virtual uint16_t vcp() const = 0; + virtual std::uint16_t vcp() const = 0; virtual const std::vector& vertices() const = 0; std::shared_ptr radar_product_manager() const; + std::chrono::system_clock::time_point selected_time() const; std::mutex& sweep_mutex(); void set_radar_product_manager( @@ -48,20 +49,24 @@ public: virtual void LoadColorTable(std::shared_ptr colorTable) = 0; virtual void SelectElevation(float elevation); - virtual void SelectProduct(const std::string& productName) = 0; - virtual void SelectTime(std::chrono::system_clock::time_point time) = 0; - virtual void Update() = 0; + virtual void SelectProduct(const std::string& productName) = 0; + void SelectTime(std::chrono::system_clock::time_point time); + virtual void Update() = 0; bool IsInitialized() const; virtual common::RadarProductGroup GetRadarProductGroup() const = 0; virtual std::string GetRadarProductName() const = 0; virtual std::vector GetElevationCuts() const; - virtual std::tuple GetMomentData() const = 0; - virtual std::tuple GetCfpMomentData() const; + virtual std::tuple + GetMomentData() const = 0; + virtual std::tuple + GetCfpMomentData() const; protected: - virtual void UpdateColorTable() = 0; + virtual void ConnectRadarProductManager() = 0; + virtual void DisconnectRadarProductManager() = 0; + virtual void UpdateColorTable() = 0; protected slots: virtual void ComputeSweep(); diff --git a/wxdata/include/scwx/util/map.hpp b/wxdata/include/scwx/util/map.hpp index 9aa2b6f0..3392b5d7 100644 --- a/wxdata/include/scwx/util/map.hpp +++ b/wxdata/include/scwx/util/map.hpp @@ -8,10 +8,10 @@ namespace scwx namespace util { -template> -ReturnType GetBoundedElement(std::map& map, Key key) +template::const_pointer> +ReturnType GetBoundedElementPointer(std::map& map, const Key& key) { - ReturnType element; + ReturnType elementPtr {nullptr}; // Find the first element greater than the key requested auto it = map.upper_bound(key); @@ -24,26 +24,42 @@ ReturnType GetBoundedElement(std::map& map, Key key) { // Get the element immediately preceding, this the element we are // looking for - element = (--it)->second; + elementPtr = &(*(--it)); } else { // The current element is a good substitute - element = it->second; + elementPtr = &(*it); } } else if (map.size() > 0) { // An element with a key greater was not found. If it exists, it must be // the last element. - element = map.rbegin()->second; + elementPtr = &(*map.rbegin()); + } + + return elementPtr; +} + +template> +ReturnType GetBoundedElement(std::map& map, const Key& key) +{ + ReturnType element; + + typename std::map::pointer elementPtr = + GetBoundedElementPointer::pointer>(map, + key); + if (elementPtr != nullptr) + { + element = elementPtr->second; } return element; } template -inline T GetBoundedElementValue(std::map& map, Key key) +inline T GetBoundedElementValue(std::map& map, const Key& key) { return GetBoundedElement(map, key); }