From 0e21884322305c81d725c5b65476c987486000b0 Mon Sep 17 00:00:00 2001 From: AdenKoperczak Date: Sat, 19 Jul 2025 09:20:00 -0400 Subject: [PATCH 01/37] Switch alert clicks to mouse button release --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index 6599e0e5..124a508e 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -883,7 +883,7 @@ void AlertLayer::Impl::HandleGeoLinesEvent( switch (ev->type()) { - case QEvent::Type::MouseButtonPress: + case QEvent::Type::MouseButtonRelease: { auto it = segmentsByLine_.find(di); if (it != segmentsByLine_.cend()) From 3c5c9ea27e4f75f3ecb2b55329e341c37419d015 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Aug 2025 18:30:23 -0500 Subject: [PATCH 02/37] Only restore geometry on first show, not restore from minimize --- scwx-qt/source/scwx/qt/main/main_window.cpp | 28 ++++++++++++++------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 4fa2f28a..723db052 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -463,18 +463,28 @@ void MainWindow::keyReleaseEvent(QKeyEvent* ev) void MainWindow::showEvent(QShowEvent* event) { QMainWindow::showEvent(event); - auto& uiSettings = settings::UiSettings::Instance(); - // restore the geometry state - std::string uiGeometry = uiSettings.main_ui_geometry().GetValue(); - restoreGeometry( - QByteArray::fromBase64(QByteArray::fromStdString(uiGeometry))); + static bool firstShowEvent = true; + bool restored = false; - // restore the UI state - std::string uiState = uiSettings.main_ui_state().GetValue(); + if (firstShowEvent) + { + auto& uiSettings = settings::UiSettings::Instance(); + + // restore the geometry state + std::string uiGeometry = uiSettings.main_ui_geometry().GetValue(); + restoreGeometry( + QByteArray::fromBase64(QByteArray::fromStdString(uiGeometry))); + + // restore the UI state + std::string uiState = uiSettings.main_ui_state().GetValue(); + + restored = restoreState( + QByteArray::fromBase64(QByteArray::fromStdString(uiState))); + + firstShowEvent = false; + } - bool restored = - restoreState(QByteArray::fromBase64(QByteArray::fromStdString(uiState))); if (!restored) { resizeDocks({ui->radarToolboxDock}, {194}, Qt::Horizontal); From b6797eee7e65296ba935d8bb63ad3b86d2da6494 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 28 Aug 2025 19:08:50 -0500 Subject: [PATCH 03/37] Adding const to uiGeometry and uiState --- scwx-qt/source/scwx/qt/main/main_window.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 723db052..331f25e1 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -472,12 +472,12 @@ void MainWindow::showEvent(QShowEvent* event) auto& uiSettings = settings::UiSettings::Instance(); // restore the geometry state - std::string uiGeometry = uiSettings.main_ui_geometry().GetValue(); + const std::string uiGeometry = uiSettings.main_ui_geometry().GetValue(); restoreGeometry( QByteArray::fromBase64(QByteArray::fromStdString(uiGeometry))); // restore the UI state - std::string uiState = uiSettings.main_ui_state().GetValue(); + const std::string uiState = uiSettings.main_ui_state().GetValue(); restored = restoreState( QByteArray::fromBase64(QByteArray::fromStdString(uiState))); From 68f66c0c2f2a75fc820eddae7e39d7779f9bd09b Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Aug 2025 15:01:00 -0500 Subject: [PATCH 04/37] Add Qt version to log --- scwx-qt/source/scwx/qt/main/main.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index a2416cec..68e7bbbc 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -71,6 +72,8 @@ int main(int argc, char* argv[]) scwx::qt::main::kVersionString_, scwx::qt::main::kBuildNumber_, scwx::qt::main::kCommitString_); + logger_->info("Qt version {}", + QLibraryInfo::version().toString().toStdString()); InitializeOpenGL(); From f4226b487de5bbc86927c01ee49f560d7291a4c5 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Aug 2025 16:27:55 -0500 Subject: [PATCH 05/37] Initial moving of product listing to the background for level 3 --- scwx-qt/scwx-qt.cmake | 1 + .../scwx/qt/manager/radar_product_manager.cpp | 254 ++++++++++++++---- .../scwx/qt/manager/radar_product_manager.hpp | 25 +- .../scwx/qt/types/radar_product_types.hpp | 16 ++ .../scwx/qt/view/level2_product_view.cpp | 18 +- .../scwx/qt/view/level3_product_view.cpp | 27 +- .../scwx/qt/view/level3_radial_view.cpp | 32 +-- .../scwx/qt/view/level3_raster_view.cpp | 12 +- .../scwx/qt/view/overlay_product_view.cpp | 28 +- .../aws_level2_chunks_data_provider.hpp | 4 +- .../provider/aws_nexrad_data_provider.hpp | 27 +- .../scwx/provider/nexrad_data_provider.hpp | 29 +- .../aws_level2_chunks_data_provider.cpp | 9 +- .../provider/aws_nexrad_data_provider.cpp | 52 ++-- 14 files changed, 356 insertions(+), 178 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/types/radar_product_types.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index ecd26ef9..1a093200 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -234,6 +234,7 @@ set(HDR_TYPES source/scwx/qt/types/alert_types.hpp source/scwx/qt/types/media_types.hpp source/scwx/qt/types/qt_types.hpp source/scwx/qt/types/radar_product_record.hpp + source/scwx/qt/types/radar_product_types.hpp source/scwx/qt/types/text_event_key.hpp source/scwx/qt/types/text_types.hpp source/scwx/qt/types/texture_types.hpp 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 bda6e232..7995968a 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -36,11 +37,7 @@ # pragma warning(pop) #endif -namespace scwx -{ -namespace qt -{ -namespace manager +namespace scwx::qt::manager { static const std::string logPrefix_ = @@ -210,7 +207,8 @@ public: std::shared_ptr> GetLevel2ProductRecords(std::chrono::system_clock::time_point time); std::tuple, - std::chrono::system_clock::time_point> + std::chrono::system_clock::time_point, + types::RadarProductLoadStatus> GetLevel3ProductRecord(const std::string& product, std::chrono::system_clock::time_point time); std::shared_ptr @@ -224,15 +222,24 @@ public: std::mutex& mutex, std::chrono::system_clock::time_point time); void - LoadProviderData(std::chrono::system_clock::time_point time, - std::shared_ptr providerManager, - RadarProductRecordMap& recordMap, - std::shared_mutex& recordMutex, - std::mutex& loadDataMutex, - const std::shared_ptr& request); - void PopulateLevel2ProductTimes(std::chrono::system_clock::time_point time); + LoadProviderData(std::chrono::system_clock::time_point time, + std::shared_ptr providerManager, + RadarProductRecordMap& recordMap, + std::shared_mutex& recordMutex, + std::mutex& loadDataMutex, + const std::shared_ptr& request); + + bool AreLevel2ProductTimesPopulated( + std::chrono::system_clock::time_point time) const; + bool + AreLevel3ProductTimesPopulated(const std::string& product, + std::chrono::system_clock::time_point time); + + void PopulateLevel2ProductTimes(std::chrono::system_clock::time_point time, + bool update = true); void PopulateLevel3ProductTimes(const std::string& product, - std::chrono::system_clock::time_point time); + std::chrono::system_clock::time_point time, + bool update = true); void UpdateAvailableProductsSync(); @@ -243,11 +250,16 @@ public: const float gateRangeOffset, std::vector& outputCoordinates); + static bool AreProductTimesPopulated( + const std::shared_ptr& providerManager, + std::chrono::system_clock::time_point time); + static void PopulateProductTimes(std::shared_ptr providerManager, RadarProductRecordMap& productRecordMap, std::shared_mutex& productRecordMutex, - std::chrono::system_clock::time_point time); + std::chrono::system_clock::time_point time, + bool update); static void LoadNexradFile(CreateNexradFileFunction load, @@ -934,32 +946,32 @@ RadarProductManager::GetActiveVolumeTimes( [&](const std::shared_ptr& provider) { // For yesterday, today and tomorrow (in parallel) - std::for_each(std::execution::par, - dates.begin(), - dates.end(), - [&](const auto& date) - { - // Don't query for a time point in the future - if (date > scwx::util::time::now()) - { - return; - } + std::for_each( + std::execution::par, + dates.begin(), + dates.end(), + [&](const auto& date) + { + // Don't query for a time point in the future + if (date > scwx::util::time::now()) + { + return; + } - // Query the provider for volume time points - auto timePoints = provider->GetTimePointsByDate(date); + // Query the provider for volume time points + auto timePoints = provider->GetTimePointsByDate(date, true); - // TODO: Note, this will miss volume times present in - // Level 2 products with a second scan + // TODO: Note, this will miss volume times present in Level 2 + // products with a second scan - // Lock the merged volume time list - std::unique_lock volumeTimesLock {volumeTimesMutex}; + // Lock the merged volume time list + const std::unique_lock volumeTimesLock {volumeTimesMutex}; - // Copy time points to the merged list - std::copy( - timePoints.begin(), - timePoints.end(), - std::inserter(volumeTimes, volumeTimes.end())); - }); + // Copy time points to the merged list + std::copy(timePoints.begin(), + timePoints.end(), + std::inserter(volumeTimes, volumeTimes.end())); + }); }); // Return merged volume times list @@ -1202,21 +1214,70 @@ void RadarProductManagerImpl::LoadNexradFile( } } +bool RadarProductManagerImpl::AreLevel2ProductTimesPopulated( + std::chrono::system_clock::time_point time) const +{ + return AreProductTimesPopulated(level2ProviderManager_, time); +} + +bool RadarProductManagerImpl::AreLevel3ProductTimesPopulated( + const std::string& product, std::chrono::system_clock::time_point time) +{ + // Get provider manager + const auto level3ProviderManager = GetLevel3ProviderManager(product); + + return AreProductTimesPopulated(level3ProviderManager, time); +} + +bool RadarProductManagerImpl::AreProductTimesPopulated( + const std::shared_ptr& providerManager, + std::chrono::system_clock::time_point time) +{ + const auto today = std::chrono::floor(time); + + bool productTimesPopulated = true; + + // Don't query for the epoch, assume populated + if (today == std::chrono::system_clock::time_point {}) + { + return productTimesPopulated; + } + + const auto yesterday = today - std::chrono::days {1}; + const auto tomorrow = today + std::chrono::days {1}; + const auto dates = {yesterday, today, tomorrow}; + + for (auto& date : dates) + { + // Don't query for a time point in the future + if (date > scwx::util::time::now()) + { + continue; + } + + if (!providerManager->provider_->IsDateCached(date)) + { + productTimesPopulated = false; + } + } + + return productTimesPopulated; +} + void RadarProductManagerImpl::PopulateLevel2ProductTimes( - std::chrono::system_clock::time_point time) + std::chrono::system_clock::time_point time, bool update) { PopulateProductTimes(level2ProviderManager_, level2ProductRecords_, level2ProductRecordMutex_, - time); - PopulateProductTimes(level2ChunksProviderManager_, - level2ProductRecords_, - level2ProductRecordMutex_, - time); + time, + update); } void RadarProductManagerImpl::PopulateLevel3ProductTimes( - const std::string& product, std::chrono::system_clock::time_point time) + const std::string& product, + std::chrono::system_clock::time_point time, + bool update) { // Get provider manager auto level3ProviderManager = GetLevel3ProviderManager(product); @@ -1229,15 +1290,23 @@ void RadarProductManagerImpl::PopulateLevel3ProductTimes( PopulateProductTimes(level3ProviderManager, level3ProductRecords, level3ProductRecordMutex_, - time); + time, + update); } void RadarProductManagerImpl::PopulateProductTimes( std::shared_ptr providerManager, RadarProductRecordMap& productRecordMap, std::shared_mutex& productRecordMutex, - std::chrono::system_clock::time_point time) + std::chrono::system_clock::time_point time, + bool update) { + logger_->debug("Populating product times (Update: {}): {}, {}, {}", + update, + common::GetRadarProductGroupName(providerManager->group_), + providerManager->product_, + scwx::util::time::TimeString(time)); + const auto today = std::chrono::floor(time); // Don't query for the epoch @@ -1267,7 +1336,8 @@ void RadarProductManagerImpl::PopulateProductTimes( // Query the provider for volume time points auto timePoints = - providerManager->provider_->GetTimePointsByDate(date); + providerManager->provider_->GetTimePointsByDate(date, + update); // Lock the merged volume time list std::unique_lock volumeTimesLock {volumeTimesMutex}; @@ -1381,16 +1451,46 @@ RadarProductManagerImpl::GetLevel2ProductRecords( } std::tuple, - std::chrono::system_clock::time_point> + std::chrono::system_clock::time_point, + types::RadarProductLoadStatus> RadarProductManagerImpl::GetLevel3ProductRecord( const std::string& product, std::chrono::system_clock::time_point time) { std::shared_ptr record {nullptr}; RadarProductRecordMap::const_pointer recordPtr {nullptr}; std::chrono::system_clock::time_point recordTime {time}; + types::RadarProductLoadStatus status { + types::RadarProductLoadStatus::ListingProducts}; // Ensure Level 3 product records are updated - PopulateLevel3ProductTimes(product, time); + if (!AreLevel3ProductTimesPopulated(product, time)) + { + logger_->debug("Level 3 product times need populated: {}, {}", + product, + scwx::util::time::TimeString(time)); + + // Populate level 3 product times asynchronously + boost::asio::post(threadPool_, + [product, time, this]() + { + // Populate product times + PopulateLevel3ProductTimes(product, time); + + // Signal finished + Q_EMIT self_->ProductTimesPopulated( + common::RadarProductGroup::Level3, product, time); + }); + + // Return listing products status + return {record, recordTime, status}; + } + else + { + PopulateLevel3ProductTimes(product, time, false); + } + + // Advance to loading product + status = types::RadarProductLoadStatus::LoadingProduct; std::unique_lock lock {level3ProductRecordMutex_}; @@ -1415,9 +1515,27 @@ RadarProductManagerImpl::GetLevel3ProductRecord( if (recordPtr != nullptr) { + using namespace std::chrono_literals; + // Don't check for an exact time match for level 3 products recordTime = recordPtr->first; - record = recordPtr->second.lock(); + + if ( + // For latest data, ensure it is from the last 24 hours + (time == std::chrono::system_clock::time_point {} && + (recordTime > scwx::util::time::now() - 24h || recordTime == time)) || + // For time queries, ensure data is within 24 hours of the request + (time != std::chrono::system_clock::time_point {} && + std::chrono::abs(recordTime - time) < 24h)) + { + record = recordPtr->second.lock(); + } + else + { + // Reset the record + recordPtr = nullptr; + recordTime = time; + } } if (recordPtr != nullptr && record == nullptr && @@ -1440,9 +1558,22 @@ RadarProductManagerImpl::GetLevel3ProductRecord( }); self_->LoadLevel3Data(product, recordTime, request); + + // Status is already set to LoadingProduct } - return {record, recordTime}; + if (recordPtr == nullptr) + { + // If the record is empty, the product is not available + status = types::RadarProductLoadStatus::ProductNotAvailable; + } + else if (record != nullptr) + { + // If the record was populated, the product has been loaded + status = types::RadarProductLoadStatus::ProductLoaded; + } + + return {record, recordTime, status}; } std::shared_ptr @@ -1543,7 +1674,8 @@ void RadarProductManagerImpl::UpdateRecentRecords( std::tuple, float, std::vector, - std::chrono::system_clock::time_point> + std::chrono::system_clock::time_point, + types::RadarProductLoadStatus> RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, float elevation, std::chrono::system_clock::time_point time) @@ -1639,25 +1771,31 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, } } - return {radarData, elevationCut, elevationCuts, foundTime}; + return {radarData, + elevationCut, + elevationCuts, + foundTime, + types::RadarProductLoadStatus::ProductLoaded}; } std::tuple, - std::chrono::system_clock::time_point> + std::chrono::system_clock::time_point, + types::RadarProductLoadStatus> RadarProductManager::GetLevel3Data(const std::string& product, std::chrono::system_clock::time_point time) { std::shared_ptr message = nullptr; + types::RadarProductLoadStatus status {}; std::shared_ptr record; - std::tie(record, time) = p->GetLevel3ProductRecord(product, time); + std::tie(record, time, status) = p->GetLevel3ProductRecord(product, time); if (record != nullptr) { message = record->level3_file()->message(); } - return {message, time}; + return {message, time, status}; } common::Level3ProductCategoryMap @@ -1809,6 +1947,4 @@ RadarProductManager::Instance(const std::string& radarSite) #include "radar_product_manager.moc" -} // namespace manager -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::manager 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 e8c72193..87bd0484 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp @@ -5,23 +5,19 @@ #include #include #include +#include #include #include #include #include #include -#include #include #include #include -namespace scwx -{ -namespace qt -{ -namespace manager +namespace scwx::qt::manager { class RadarProductManagerImpl; @@ -89,12 +85,13 @@ public: * @param [in] time Radar product time * * @return Level 2 radar data, selected elevation cut, available elevation - * cuts and selected time + * cuts, selected time and product load status */ std::tuple, float, std::vector, - std::chrono::system_clock::time_point> + std::chrono::system_clock::time_point, + types::RadarProductLoadStatus> GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, float elevation, std::chrono::system_clock::time_point time = {}); @@ -105,10 +102,11 @@ public: * @param [in] product Radar product name * @param [in] time Radar product time * - * @return Level 3 message data and selected time + * @return Level 3 message data, selected time and product load status */ std::tuple, - std::chrono::system_clock::time_point> + std::chrono::system_clock::time_point, + types::RadarProductLoadStatus> GetLevel3Data(const std::string& product, std::chrono::system_clock::time_point time = {}); @@ -151,6 +149,9 @@ signals: bool isChunks, std::chrono::system_clock::time_point latestTime); void IncomingLevel2ElevationChanged(std::optional incomingElevation); + void ProductTimesPopulated(common::RadarProductGroup group, + const std::string& product, + std::chrono::system_clock::time_point queryTime); private: std::unique_ptr p; @@ -158,6 +159,4 @@ private: friend class RadarProductManagerImpl; }; -} // namespace manager -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::manager diff --git a/scwx-qt/source/scwx/qt/types/radar_product_types.hpp b/scwx-qt/source/scwx/qt/types/radar_product_types.hpp new file mode 100644 index 00000000..2a1f568e --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/radar_product_types.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace scwx::qt::types +{ + +enum class RadarProductLoadStatus : std::uint8_t +{ + ProductLoaded, + ListingProducts, + LoadingProduct, + ProductNotAvailable +}; + +} 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 9ead358b..eadf3615 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -11,11 +11,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace view +namespace scwx::qt::view { static const std::string logPrefix_ = "scwx::qt::view::level2_product_view"; @@ -552,7 +548,11 @@ void Level2ProductView::ComputeSweep() std::shared_ptr radarData; std::chrono::system_clock::time_point requestedTime {selected_time()}; - std::tie(radarData, p->elevationCut_, p->elevationCuts_, std::ignore) = + std::tie(radarData, + p->elevationCut_, + p->elevationCuts_, + std::ignore, + std::ignore) = radarProductManager->GetLevel2Data( p->dataBlockType_, p->selectedElevation_, requestedTime); @@ -1369,7 +1369,7 @@ Level2ProductView::GetBinLevel(const common::Coordinate& coordinate) const auto nextRadial = radarData->find((i + 1) % numRadials); if (nextRadial != radarData->cend()) { - nextAngle = nextRadial->second->azimuth_angle(); + nextAngle = nextRadial->second->azimuth_angle(); // Level 2 angles are the center of the bins. const units::degrees deltaAngle = @@ -1564,6 +1564,4 @@ std::shared_ptr Level2ProductView::Create( return std::make_shared(product, radarProductManager); } -} // namespace view -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::view 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 97985a39..6abe7556 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp @@ -11,17 +11,12 @@ #include #include -#include #include #include #include -namespace scwx -{ -namespace qt -{ -namespace view +namespace scwx::qt::view { static const std::string logPrefix_ = "scwx::qt::view::level3_product_view"; @@ -151,6 +146,22 @@ void Level3ProductView::ConnectRadarProductManager() Update(); } }); + + connect(radar_product_manager().get(), + &manager::RadarProductManager::ProductTimesPopulated, + this, + [this](common::RadarProductGroup group, + const std::string& product, + std::chrono::system_clock::time_point queryTime) + { + if (group == common::RadarProductGroup::Level3 && + product == p->product_ && queryTime == selected_time()) + { + // If the data associated with the currently selected time is + // reloaded, update the view + Update(); + } + }); } void Level3ProductView::DisconnectRadarProductManager() @@ -596,6 +607,4 @@ bool Level3ProductView::IgnoreUnits() const return false; } -} // namespace view -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::view 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 c01e0cd4..60eb780e 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -10,11 +10,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace view +namespace scwx::qt::view { static const std::string logPrefix_ = "scwx::qt::view::level3_radial_view"; @@ -31,15 +27,7 @@ static constexpr std::uint32_t VALUES_PER_VERTEX = 2u; class Level3RadialView::Impl { public: - explicit Impl(Level3RadialView* self) : - self_ {self}, - latitude_ {}, - longitude_ {}, - range_ {}, - vcp_ {}, - sweepTime_ {} - { - } + explicit Impl(Level3RadialView* self) : self_ {self} {} ~Impl() { threadPool_.join(); }; void ComputeCoordinates( @@ -65,13 +53,13 @@ public: bool lastShowSmoothedRangeFolding_ {false}; bool lastSmoothingEnabled_ {false}; - float latitude_; - float longitude_; + float latitude_ {}; + float longitude_ {}; std::optional elevation_ {}; - float range_; - std::uint16_t vcp_; + float range_ {}; + std::uint16_t vcp_ {}; - std::chrono::system_clock::time_point sweepTime_; + std::chrono::system_clock::time_point sweepTime_ {}; }; Level3RadialView::Level3RadialView( @@ -148,7 +136,7 @@ void Level3RadialView::ComputeSweep() std::shared_ptr message; std::chrono::system_clock::time_point requestedTime {selected_time()}; std::chrono::system_clock::time_point foundTime; - std::tie(message, foundTime) = + std::tie(message, foundTime, std::ignore) = radarProductManager->GetLevel3Data(GetRadarProductName(), requestedTime); // If a different time was found than what was requested, update it @@ -752,6 +740,4 @@ std::shared_ptr Level3RadialView::Create( return std::make_shared(product, radarProductManager); } -} // namespace view -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::view 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 3056cc03..75aa1c0d 100644 --- a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp @@ -10,11 +10,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace view +namespace scwx::qt::view { static const std::string logPrefix_ = "scwx::qt::view::level3_raster_view"; @@ -125,7 +121,7 @@ void Level3RasterView::ComputeSweep() std::shared_ptr message; std::chrono::system_clock::time_point requestedTime {selected_time()}; std::chrono::system_clock::time_point foundTime; - std::tie(message, foundTime) = + std::tie(message, foundTime, std::ignore) = radarProductManager->GetLevel3Data(GetRadarProductName(), requestedTime); // If a different time was found than what was requested, update it @@ -538,6 +534,4 @@ std::shared_ptr Level3RasterView::Create( return std::make_shared(product, radarProductManager); } -} // namespace view -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::view diff --git a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp index ccc41b9d..95f1af09 100644 --- a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp @@ -8,11 +8,7 @@ #include #include -namespace scwx -{ -namespace qt -{ -namespace view +namespace scwx::qt::view { static const std::string logPrefix_ = "scwx::qt::view::overlay_product_view"; @@ -128,6 +124,22 @@ void OverlayProductView::Impl::ConnectRadarProductManager() } }, Qt::QueuedConnection); + + connect(radarProductManager_.get(), + &manager::RadarProductManager::ProductTimesPopulated, + self_, + [this](common::RadarProductGroup group, + const std::string& product, + std::chrono::system_clock::time_point queryTime) + { + if (group == common::RadarProductGroup::Level3 && + product == kNst_ && queryTime == selectedTime_) + { + // If the data associated with the currently selected time is + // reloaded, update the view + Update(product); + } + }); } void OverlayProductView::Impl::DisconnectRadarProductManager() @@ -286,7 +298,7 @@ void OverlayProductView::Impl::Update(const std::string& product) std::shared_ptr message; std::chrono::system_clock::time_point requestedTime {selectedTime_}; std::chrono::system_clock::time_point foundTime; - std::tie(message, foundTime) = + std::tie(message, foundTime, std::ignore) = radarProductManager_->GetLevel3Data(product, requestedTime); // If a different time was found than what was requested, update it @@ -329,6 +341,4 @@ void OverlayProductView::SetAutoUpdate(bool enabled) p->autoUpdateEnabled_ = enabled; } -} // namespace view -} // namespace qt -} // namespace scwx +} // namespace scwx::qt::view diff --git a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp index abd70787..1c8a1ca9 100644 --- a/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp +++ b/wxdata/include/scwx/provider/aws_level2_chunks_data_provider.hpp @@ -46,7 +46,9 @@ public: std::string FindLatestKey() override; std::chrono::system_clock::time_point FindLatestTime() override; std::vector - GetTimePointsByDate(std::chrono::system_clock::time_point date) override; + GetTimePointsByDate(std::chrono::system_clock::time_point date, + bool update) override; + bool IsDateCached(std::chrono::system_clock::time_point date) override; std::tuple ListObjects(std::chrono::system_clock::time_point date) override; std::shared_ptr diff --git a/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp b/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp index d6ddcd7c..3db8c273 100644 --- a/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp +++ b/wxdata/include/scwx/provider/aws_nexrad_data_provider.hpp @@ -2,17 +2,12 @@ #include -namespace Aws -{ -namespace S3 +namespace Aws::S3 { class S3Client; -} // namespace S3 -} // namespace Aws +} // namespace Aws::S3 -namespace scwx -{ -namespace provider +namespace scwx::provider { /** @@ -32,20 +27,23 @@ public: AwsNexradDataProvider(AwsNexradDataProvider&&) noexcept; AwsNexradDataProvider& operator=(AwsNexradDataProvider&&) noexcept; - size_t cache_size() const override; + [[nodiscard]] std::size_t cache_size() const override; - std::chrono::system_clock::time_point last_modified() const override; - std::chrono::seconds update_period() const override; + [[nodiscard]] std::chrono::system_clock::time_point + last_modified() const override; + [[nodiscard]] std::chrono::seconds update_period() const override; std::string FindKey(std::chrono::system_clock::time_point time) override; std::string FindLatestKey() override; std::chrono::system_clock::time_point FindLatestTime() override; std::vector - GetTimePointsByDate(std::chrono::system_clock::time_point date) override; + GetTimePointsByDate(std::chrono::system_clock::time_point date, + bool update) override; + bool IsDateCached(std::chrono::system_clock::time_point date) override; std::tuple ListObjects(std::chrono::system_clock::time_point date) override; std::shared_ptr - LoadObjectByKey(const std::string& key) override; + LoadObjectByKey(const std::string& key) override; std::shared_ptr LoadObjectByTime(std::chrono::system_clock::time_point time) override; std::pair Refresh() override; @@ -61,5 +59,4 @@ private: std::unique_ptr p; }; -} // namespace provider -} // namespace scwx +} // namespace scwx::provider diff --git a/wxdata/include/scwx/provider/nexrad_data_provider.hpp b/wxdata/include/scwx/provider/nexrad_data_provider.hpp index 2a7320d2..2bc3703d 100644 --- a/wxdata/include/scwx/provider/nexrad_data_provider.hpp +++ b/wxdata/include/scwx/provider/nexrad_data_provider.hpp @@ -7,9 +7,7 @@ #include #include -namespace scwx -{ -namespace provider +namespace scwx::provider { class NexradDataProvider @@ -24,7 +22,7 @@ public: NexradDataProvider(NexradDataProvider&&) noexcept; NexradDataProvider& operator=(NexradDataProvider&&) noexcept; - virtual size_t cache_size() const = 0; + [[nodiscard]] virtual size_t cache_size() const = 0; /** * Gets the last modified time. This is equal to the most recent object's @@ -32,7 +30,8 @@ public: * * @return Last modified time */ - virtual std::chrono::system_clock::time_point last_modified() const = 0; + [[nodiscard]] virtual std::chrono::system_clock::time_point + last_modified() const = 0; /** * Gets the current update period. This is equal to the difference between @@ -41,7 +40,7 @@ public: * * @return Update period */ - virtual std::chrono::seconds update_period() const = 0; + [[nodiscard]] virtual std::chrono::seconds update_period() const = 0; /** * Finds the most recent key in the cache, no later than the time provided. @@ -116,7 +115,7 @@ public: * * @return NEXRAD data time point */ - virtual std::chrono::system_clock::time_point + [[nodiscard]] virtual std::chrono::system_clock::time_point GetTimePointByKey(const std::string& key) const = 0; /** @@ -124,11 +123,22 @@ public: * to the cache if required. * * @param date Date for which to get NEXRAD data time points + * @param update Whether or not to list and add data not present in the cache * * @return NEXRAD data time points */ virtual std::vector - GetTimePointsByDate(std::chrono::system_clock::time_point date) = 0; + GetTimePointsByDate(std::chrono::system_clock::time_point date, + bool update) = 0; + + /** + * Determines if time points for the requested date are cached. + * + * @param date Date for which to query the cache + * + * @return Whether or not the requested date is cached + */ + virtual bool IsDateCached(std::chrono::system_clock::time_point date) = 0; /** * Requests available NEXRAD products for the current radar site, and adds @@ -148,5 +158,4 @@ private: std::unique_ptr p; }; -} // namespace provider -} // namespace scwx +} // namespace scwx::provider diff --git a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp index f7de36ca..e5f99b99 100644 --- a/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_chunks_data_provider.cpp @@ -289,11 +289,18 @@ AwsLevel2ChunksDataProvider::FindLatestTime() std::vector AwsLevel2ChunksDataProvider::GetTimePointsByDate( - std::chrono::system_clock::time_point /*date*/) + std::chrono::system_clock::time_point /* date */, bool /* update */) { return {}; } +bool AwsLevel2ChunksDataProvider::IsDateCached( + std::chrono::system_clock::time_point /* date */) +{ + // No cache, default to true + return true; +} + std::chrono::system_clock::time_point AwsLevel2ChunksDataProvider::Impl::GetScanTime(const std::string& prefix) { diff --git a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp index 97528a9e..b7eab596 100644 --- a/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_nexrad_data_provider.cpp @@ -15,9 +15,7 @@ #include #include -namespace scwx -{ -namespace provider +namespace scwx::provider { static const std::string logPrefix_ = @@ -177,7 +175,7 @@ std::chrono::system_clock::time_point AwsNexradDataProvider::FindLatestTime() std::vector AwsNexradDataProvider::GetTimePointsByDate( - std::chrono::system_clock::time_point date) + std::chrono::system_clock::time_point date, bool update) { const auto day = std::chrono::floor(date); @@ -188,23 +186,26 @@ AwsNexradDataProvider::GetTimePointsByDate( std::shared_lock lock(p->objectsMutex_); // Is the date present in the date list? - bool currentDatePresent; + bool currentDatePresent = false; auto currentDateIterator = std::find(p->objectDates_.cbegin(), p->objectDates_.cend(), day); if (currentDateIterator == p->objectDates_.cend()) { - // Temporarily unlock mutex - lock.unlock(); - - // List objects, since the date is not present in the date list - auto [success, newObjects, totalObjects] = ListObjects(date); - if (success) + if (update) { - p->UpdateObjectDates(date); - } + // Temporarily unlock mutex + lock.unlock(); - // Re-lock mutex - lock.lock(); + // List objects, since the date is not present in the date list + const auto [success, newObjects, totalObjects] = ListObjects(date); + if (success) + { + p->UpdateObjectDates(date); + } + + // Re-lock mutex + lock.lock(); + } currentDatePresent = false; } @@ -214,8 +215,8 @@ AwsNexradDataProvider::GetTimePointsByDate( } // Determine objects to retrieve - auto objectsBegin = p->objects_.lower_bound(day); - auto objectsEnd = p->objects_.lower_bound(day + std::chrono::days {1}); + const auto objectsBegin = p->objects_.lower_bound(day); + const auto objectsEnd = p->objects_.lower_bound(day + std::chrono::days {1}); // Copy time points to destination vector std::transform(objectsBegin, @@ -236,6 +237,20 @@ AwsNexradDataProvider::GetTimePointsByDate( return timePoints; } +bool AwsNexradDataProvider::IsDateCached( + std::chrono::system_clock::time_point date) +{ + const auto day = std::chrono::floor(date); + + const std::shared_lock lock(p->objectsMutex_); + + // Is the date present in the date list? + const auto currentDateIterator = + std::find(p->objectDates_.cbegin(), p->objectDates_.cend(), day); + + return currentDateIterator != p->objectDates_.cend(); +} + std::tuple AwsNexradDataProvider::ListObjects(std::chrono::system_clock::time_point date) { @@ -446,5 +461,4 @@ void AwsNexradDataProvider::Impl::UpdateObjectDates( objectDates_.push_back(day); } -} // namespace provider -} // namespace scwx +} // namespace scwx::provider From 449d8cb796298024300ee25ed0c249d77f86a421 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Aug 2025 18:21:16 -0500 Subject: [PATCH 06/37] A query for the epoch should be treated as time now --- .../source/scwx/qt/manager/radar_product_manager.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 7995968a..8806d217 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -1233,14 +1233,14 @@ bool RadarProductManagerImpl::AreProductTimesPopulated( const std::shared_ptr& providerManager, std::chrono::system_clock::time_point time) { - const auto today = std::chrono::floor(time); + auto today = std::chrono::floor(time); bool productTimesPopulated = true; - // Don't query for the epoch, assume populated + // Assume a query for the epoch is a query for now if (today == std::chrono::system_clock::time_point {}) { - return productTimesPopulated; + today = std::chrono::floor(scwx::util::time::now()); } const auto yesterday = today - std::chrono::days {1}; @@ -1307,12 +1307,12 @@ void RadarProductManagerImpl::PopulateProductTimes( providerManager->product_, scwx::util::time::TimeString(time)); - const auto today = std::chrono::floor(time); + auto today = std::chrono::floor(time); - // Don't query for the epoch + // Assume a query for the epoch is a query for now if (today == std::chrono::system_clock::time_point {}) { - return; + today = std::chrono::floor(scwx::util::time::now()); } const auto yesterday = today - std::chrono::days {1}; From a306fb4363cc7fe6241c4f2398e5b29589b4f5a1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Aug 2025 18:24:50 -0500 Subject: [PATCH 07/37] Add NoUpdateReason of NotAvailable, driven by ProductNotAvailable --- .../source/scwx/qt/manager/timeline_manager.cpp | 3 ++- scwx-qt/source/scwx/qt/types/map_types.hpp | 1 + .../source/scwx/qt/types/radar_product_types.hpp | 1 + .../source/scwx/qt/view/level2_product_view.cpp | 15 +++++++++------ .../source/scwx/qt/view/level3_radial_view.cpp | 10 ++++++++-- .../source/scwx/qt/view/level3_raster_view.cpp | 10 ++++++++-- .../source/scwx/qt/view/radar_product_view.cpp | 12 ++++++++++++ .../source/scwx/qt/view/radar_product_view.hpp | 13 ++++++++----- 8 files changed, 49 insertions(+), 16 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp index 70c4b2ed..6a8765ae 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -317,7 +317,8 @@ void TimelineManager::ReceiveRadarSweepNotUpdated(std::size_t mapIndex, types::NoUpdateReason reason) { if (!p->radarSweepMonitorActive_ || - reason == types::NoUpdateReason::NotLoaded) + (reason == types::NoUpdateReason::NotLoaded || + reason == types::NoUpdateReason::NotAvailable)) { return; } diff --git a/scwx-qt/source/scwx/qt/types/map_types.hpp b/scwx-qt/source/scwx/qt/types/map_types.hpp index 5853a188..e8c2a24f 100644 --- a/scwx-qt/source/scwx/qt/types/map_types.hpp +++ b/scwx-qt/source/scwx/qt/types/map_types.hpp @@ -25,6 +25,7 @@ enum class NoUpdateReason { NoChange, NotLoaded, + NotAvailable, InvalidProduct, InvalidData }; diff --git a/scwx-qt/source/scwx/qt/types/radar_product_types.hpp b/scwx-qt/source/scwx/qt/types/radar_product_types.hpp index 2a1f568e..7e9fd70c 100644 --- a/scwx-qt/source/scwx/qt/types/radar_product_types.hpp +++ b/scwx-qt/source/scwx/qt/types/radar_product_types.hpp @@ -7,6 +7,7 @@ namespace scwx::qt::types enum class RadarProductLoadStatus : std::uint8_t { + ProductNotLoaded, ProductLoaded, ListingProducts, LoadingProduct, 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 eadf3615..a9ea3b00 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -548,17 +548,20 @@ void Level2ProductView::ComputeSweep() std::shared_ptr radarData; std::chrono::system_clock::time_point requestedTime {selected_time()}; - std::tie(radarData, - p->elevationCut_, - p->elevationCuts_, - std::ignore, - std::ignore) = + types::RadarProductLoadStatus loadStatus {}; + std::tie( + radarData, p->elevationCut_, p->elevationCuts_, std::ignore, loadStatus) = radarProductManager->GetLevel2Data( p->dataBlockType_, p->selectedElevation_, requestedTime); + set_load_status(loadStatus); + if (radarData == nullptr) { - Q_EMIT SweepNotComputed(types::NoUpdateReason::NotLoaded); + Q_EMIT SweepNotComputed( + loadStatus == types::RadarProductLoadStatus::ProductNotAvailable ? + types::NoUpdateReason::NotAvailable : + types::NoUpdateReason::NotLoaded); return; } if ((radarData == p->elevationScan_) && 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 60eb780e..b8fe539b 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -136,9 +136,12 @@ void Level3RadialView::ComputeSweep() std::shared_ptr message; std::chrono::system_clock::time_point requestedTime {selected_time()}; std::chrono::system_clock::time_point foundTime; - std::tie(message, foundTime, std::ignore) = + types::RadarProductLoadStatus loadStatus {}; + std::tie(message, foundTime, loadStatus) = radarProductManager->GetLevel3Data(GetRadarProductName(), requestedTime); + set_load_status(loadStatus); + // If a different time was found than what was requested, update it if (requestedTime != foundTime) { @@ -148,7 +151,10 @@ void Level3RadialView::ComputeSweep() if (message == nullptr) { logger_->debug("Level 3 data not found"); - Q_EMIT SweepNotComputed(types::NoUpdateReason::NotLoaded); + Q_EMIT SweepNotComputed( + loadStatus == types::RadarProductLoadStatus::ProductNotAvailable ? + types::NoUpdateReason::NotAvailable : + types::NoUpdateReason::NotLoaded); return; } 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 75aa1c0d..92ecbb9f 100644 --- a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp @@ -121,9 +121,12 @@ void Level3RasterView::ComputeSweep() std::shared_ptr message; std::chrono::system_clock::time_point requestedTime {selected_time()}; std::chrono::system_clock::time_point foundTime; - std::tie(message, foundTime, std::ignore) = + types::RadarProductLoadStatus loadStatus {}; + std::tie(message, foundTime, loadStatus) = radarProductManager->GetLevel3Data(GetRadarProductName(), requestedTime); + set_load_status(loadStatus); + // If a different time was found than what was requested, update it if (requestedTime != foundTime) { @@ -133,7 +136,10 @@ void Level3RasterView::ComputeSweep() if (message == nullptr) { logger_->debug("Level 3 data not found"); - Q_EMIT SweepNotComputed(types::NoUpdateReason::NotLoaded); + Q_EMIT SweepNotComputed( + loadStatus == types::RadarProductLoadStatus::ProductNotAvailable ? + types::NoUpdateReason::NotAvailable : + types::NoUpdateReason::NotLoaded); return; } 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 dc50383c..00e261eb 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.cpp @@ -58,6 +58,8 @@ public: std::chrono::system_clock::time_point selectedTime_; bool showSmoothedRangeFolding_ {false}; bool smoothingEnabled_ {false}; + types::RadarProductLoadStatus loadStatus_ { + types::RadarProductLoadStatus::ProductNotLoaded}; std::shared_ptr radarProductManager_; @@ -90,6 +92,11 @@ std::optional RadarProductView::elevation() const return {}; } +types::RadarProductLoadStatus RadarProductView::load_status() const +{ + return p->loadStatus_; +} + std::shared_ptr RadarProductView::radar_product_manager() const { @@ -126,6 +133,11 @@ std::mutex& RadarProductView::sweep_mutex() return p->sweepMutex_; } +void RadarProductView::set_load_status(types::RadarProductLoadStatus loadStatus) +{ + p->loadStatus_ = loadStatus; +} + void RadarProductView::set_radar_product_manager( std::shared_ptr radarProductManager) { 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 2801b74e..267afe0a 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp @@ -38,11 +38,12 @@ public: [[nodiscard]] virtual std::shared_ptr color_table() const = 0; [[nodiscard]] virtual const std::vector& - color_table_lut() const; - [[nodiscard]] virtual std::uint16_t color_table_min() const; - [[nodiscard]] virtual std::uint16_t color_table_max() const; - [[nodiscard]] virtual std::optional elevation() const; - [[nodiscard]] virtual float range() const; + color_table_lut() const; + [[nodiscard]] virtual std::uint16_t color_table_min() const; + [[nodiscard]] virtual std::uint16_t color_table_max() const; + [[nodiscard]] virtual std::optional elevation() const; + [[nodiscard]] types::RadarProductLoadStatus load_status() const; + [[nodiscard]] virtual float range() const; [[nodiscard]] virtual std::chrono::system_clock::time_point sweep_time() const; [[nodiscard]] virtual float unit_scale() const = 0; @@ -98,6 +99,8 @@ protected: virtual void DisconnectRadarProductManager() = 0; virtual void UpdateColorTableLut() = 0; + void set_load_status(types::RadarProductLoadStatus loadStatus); + protected slots: virtual void ComputeSweep(); From f679f37fc1b73e9755a00ad22eebe95d76d336a1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Aug 2025 18:26:10 -0500 Subject: [PATCH 08/37] Hide the radar sweep if no data is available --- .../scwx/qt/map/radar_product_layer.cpp | 71 +++++++++++++------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 22b43a87..3ed68897 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -146,6 +146,21 @@ void RadarProductLayer::Initialize( &view::RadarProductView::SweepComputed, this, [this]() { p->sweepNeedsUpdate_ = true; }); + connect(radarProductView.get(), + &view::RadarProductView::SweepNotComputed, + this, + [this](types::NoUpdateReason reason) + { + if (reason == types::NoUpdateReason::NotAvailable) + { + // Ensure the radar product is hidden by re-rendering + Q_EMIT NeedsRendering(); + } + if (reason == types::NoUpdateReason::NoChange) + { + Q_EMIT NeedsRendering(); + } + }); } void RadarProductLayer::UpdateSweep( @@ -281,34 +296,46 @@ void RadarProductLayer::Render( UpdateSweep(mapContext); } - const float scale = std::pow(2.0, params.zoom) * 2.0f * - mbgl::util::tileSize_D / mbgl::util::DEGREES_MAX; - const float xScale = scale / params.width; - const float yScale = scale / params.height; + std::shared_ptr radarProductView = + mapContext->radar_product_view(); - glm::mat4 uMVPMatrix(1.0f); - uMVPMatrix = glm::scale(uMVPMatrix, glm::vec3(xScale, yScale, 1.0f)); - uMVPMatrix = glm::rotate(uMVPMatrix, - glm::radians(params.bearing), - glm::vec3(0.0f, 0.0f, 1.0f)); + const bool sweepVisible = + radarProductView != nullptr && + radarProductView->load_status() != + types::RadarProductLoadStatus::ProductNotAvailable; - glUniform2fv(p->uMapScreenCoordLocation_, - 1, - glm::value_ptr(util::maplibre::LatLongToScreenCoordinate( - {params.latitude, params.longitude}))); + if (sweepVisible) + { + const float scale = std::pow(2.0, params.zoom) * 2.0f * + mbgl::util::tileSize_D / mbgl::util::DEGREES_MAX; + const float xScale = scale / params.width; + const float yScale = scale / params.height; - glUniformMatrix4fv( - p->uMVPMatrixLocation_, 1, GL_FALSE, glm::value_ptr(uMVPMatrix)); + glm::mat4 uMVPMatrix(1.0f); + uMVPMatrix = glm::scale(uMVPMatrix, glm::vec3(xScale, yScale, 1.0f)); + uMVPMatrix = glm::rotate(uMVPMatrix, + glm::radians(params.bearing), + glm::vec3(0.0f, 0.0f, 1.0f)); - glUniform1i(p->uCFPEnabledLocation_, p->cfpEnabled_ ? 1 : 0); + glUniform2fv(p->uMapScreenCoordLocation_, + 1, + glm::value_ptr(util::maplibre::LatLongToScreenCoordinate( + {params.latitude, params.longitude}))); - glUniform1ui(p->uDataMomentOffsetLocation_, p->rangeMin_); - glUniform1f(p->uDataMomentScaleLocation_, p->scale_); + glUniformMatrix4fv( + p->uMVPMatrixLocation_, 1, GL_FALSE, glm::value_ptr(uMVPMatrix)); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_1D, p->texture_); - glBindVertexArray(p->vao_); - glDrawArrays(GL_TRIANGLES, 0, static_cast(p->numVertices_)); + glUniform1i(p->uCFPEnabledLocation_, p->cfpEnabled_ ? 1 : 0); + + glUniform1ui(p->uDataMomentOffsetLocation_, p->rangeMin_); + glUniform1f(p->uDataMomentScaleLocation_, p->scale_); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_1D, p->texture_); + glBindVertexArray(p->vao_); + + glDrawArrays(GL_TRIANGLES, 0, static_cast(p->numVertices_)); + } if (wireframeEnabled) { From 341096af1dd013036f2deed67eb521c5ed7d90bf Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Aug 2025 18:28:46 -0500 Subject: [PATCH 09/37] Render data not available status in the upper right (overlay layer) --- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 135 +++++++++++++------ 1 file changed, 92 insertions(+), 43 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 04b0890e..17374188 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -92,8 +92,15 @@ public: Impl(const Impl&&) = delete; Impl& operator=(const Impl&&) = delete; + void RenderProductName(const std::shared_ptr& mapContext); + void + RenderProductDetails(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters& params); + void RenderAttribution(const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters& params); + void SetupGeoIcons(); - void SetCusorLocation(common::Coordinate coordinate); + void SetCursorLocation(common::Coordinate coordinate); OverlayLayer* self_; @@ -173,7 +180,7 @@ OverlayLayer::~OverlayLayer() p->cursorScaleConnection_.disconnect(); } -void OverlayLayer::Impl::SetCusorLocation(common::Coordinate coordinate) +void OverlayLayer::Impl::SetCursorLocation(common::Coordinate coordinate) { geoIcons_->SetIconLocation( cursorIcon_, coordinate.latitude_, coordinate.longitude_); @@ -388,7 +395,7 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, p->geoIcons_->SetIconVisible(p->cursorIcon_, cursorIconVisible); if (cursorIconVisible) { - p->SetCusorLocation(mapContext->mouse_coordinate()); + p->SetCursorLocation(mapContext->mouse_coordinate()); } // Location Icon @@ -425,6 +432,51 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, } } + p->RenderProductName(mapContext); + p->RenderProductDetails(mapContext, params); + + // Map Center Icon + if (params.width != p->lastWidth_ || params.height != p->lastHeight_) + { + // Draw the icon in the center of the widget + p->icons_->SetIconLocation( + p->mapCenterIcon_, params.width / 2.0, params.height / 2.0); + } + p->icons_->SetIconVisible(p->mapCenterIcon_, + generalSettings.show_map_center().GetValue()); + + const QMargins colorTableMargins = mapContext->color_table_margins(); + if (colorTableMargins != p->lastColorTableMargins_ || p->firstRender_) + { + // Draw map logo with a 10x10 indent from the bottom left + p->icons_->SetIconLocation(p->mapLogoIcon_, + 10 + colorTableMargins.left(), + 10 + colorTableMargins.bottom()); + } + p->icons_->SetIconVisible(p->mapLogoIcon_, + generalSettings.show_map_logo().GetValue()); + + DrawLayer::RenderWithoutImGui(params); + + p->RenderAttribution(mapContext, params); + + p->firstRender_ = false; + p->lastWidth_ = params.width; + p->lastHeight_ = params.height; + p->lastBearing_ = params.bearing; + p->lastFontSize_ = ImGui::GetFontSize(); + p->lastColorTableMargins_ = colorTableMargins; + + ImGuiFrameEnd(); + + SCWX_GL_CHECK_ERROR(); +} + +void OverlayLayer::Impl::RenderProductName( + const std::shared_ptr& mapContext) +{ + auto radarProductView = mapContext->radar_product_view(); + if (radarProductView != nullptr) { // Render product name @@ -456,14 +508,37 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, ImGui::End(); } } +} - if (p->sweepTimeString_.length() > 0) +void OverlayLayer::Impl::RenderProductDetails( + const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters& params) +{ + auto radarProductView = mapContext->radar_product_view(); + + ImGui::SetNextWindowPos(ImVec2 {static_cast(params.width), 0.0f}, + ImGuiCond_Always, + ImVec2 {1.0f, 0.0f}); + + if (radarProductView != nullptr && + radarProductView->load_status() == + types::RadarProductLoadStatus::ProductNotAvailable) + { + ImGui::Begin("Product Details", + nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_AlwaysAutoResize); + + ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 0, 0, 255)); + ImGui::TextUnformatted("NO DATA AVAILABLE"); + ImGui::PopStyleColor(); + + ImGui::End(); + } + else if (sweepTimeString_.length() > 0) { // Render time - ImGui::SetNextWindowPos(ImVec2 {static_cast(params.width), 0.0f}, - ImGuiCond_Always, - ImVec2 {1.0f, 0.0f}); - ImGui::Begin("Sweep Time", + ImGui::Begin("Product Details", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize); @@ -471,12 +546,12 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, if (radarProductView != nullptr && ImGui::IsWindowHovered()) { // Show a detailed product description when the sweep time is hovered - p->sweepTimePicked_ = true; + sweepTimePicked_ = true; auto fields = radarProductView->GetDescriptionFields(); if (fields.empty()) { - ImGui::TextUnformatted(p->sweepTimeString_.c_str()); + ImGui::TextUnformatted(sweepTimeString_.c_str()); } else { @@ -496,34 +571,19 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, } else { - ImGui::TextUnformatted(p->sweepTimeString_.c_str()); + ImGui::TextUnformatted(sweepTimeString_.c_str()); } ImGui::End(); } +} - // Map Center Icon - if (params.width != p->lastWidth_ || params.height != p->lastHeight_) - { - // Draw the icon in the center of the widget - p->icons_->SetIconLocation( - p->mapCenterIcon_, params.width / 2.0, params.height / 2.0); - } - p->icons_->SetIconVisible(p->mapCenterIcon_, - generalSettings.show_map_center().GetValue()); - +void OverlayLayer::Impl::RenderAttribution( + const std::shared_ptr& mapContext, + const QMapLibre::CustomLayerRenderParameters& params) +{ const QMargins colorTableMargins = mapContext->color_table_margins(); - if (colorTableMargins != p->lastColorTableMargins_ || p->firstRender_) - { - // Draw map logo with a 10x10 indent from the bottom left - p->icons_->SetIconLocation(p->mapLogoIcon_, - 10 + colorTableMargins.left(), - 10 + colorTableMargins.bottom()); - } - p->icons_->SetIconVisible(p->mapLogoIcon_, - generalSettings.show_map_logo().GetValue()); - - DrawLayer::RenderWithoutImGui(params); + auto& generalSettings = settings::GeneralSettings::Instance(); auto mapCopyrights = mapContext->map_copyrights(); if (mapCopyrights.length() > 0 && @@ -550,17 +610,6 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, ImGui::PopFont(); ImGui::PopStyleVar(); } - - p->firstRender_ = false; - p->lastWidth_ = params.width; - p->lastHeight_ = params.height; - p->lastBearing_ = params.bearing; - p->lastFontSize_ = ImGui::GetFontSize(); - p->lastColorTableMargins_ = colorTableMargins; - - ImGuiFrameEnd(); - - SCWX_GL_CHECK_ERROR(); } void OverlayLayer::Deinitialize() From 931dd2d0a78d749a2f3061c506ff2cf637402bad Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Aug 2025 20:30:32 -0500 Subject: [PATCH 10/37] Store last load status --- .../source/scwx/qt/map/radar_product_layer.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 3ed68897..6e315a4a 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -63,6 +63,9 @@ public: bool colorTableNeedsUpdate_ {false}; bool sweepNeedsUpdate_ {false}; + + types::RadarProductLoadStatus lastLoadStatus_ { + types::RadarProductLoadStatus::ProductNotAvailable}; }; RadarProductLayer::RadarProductLayer(std::shared_ptr glContext) : @@ -158,7 +161,12 @@ void RadarProductLayer::Initialize( } if (reason == types::NoUpdateReason::NoChange) { - Q_EMIT NeedsRendering(); + if (p->lastLoadStatus_ == + types::RadarProductLoadStatus::ProductNotAvailable) + { + // Ensure the radar product is shown by re-rendering + Q_EMIT NeedsRendering(); + } } }); } @@ -273,7 +281,6 @@ void RadarProductLayer::Render( const std::shared_ptr& mapContext, const QMapLibre::CustomLayerRenderParameters& params) { - p->shaderProgram_->Use(); // Set OpenGL blend mode for transparency @@ -343,6 +350,12 @@ void RadarProductLayer::Render( glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } + if (radarProductView != nullptr) + { + // Save last load status + p->lastLoadStatus_ = radarProductView->load_status(); + } + SCWX_GL_CHECK_ERROR(); } From 41bf7f731fb0f2667247e29f243fcc9721650953 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Aug 2025 20:34:32 -0500 Subject: [PATCH 11/37] RadarProductLayer clang-tidy fixes --- .../scwx/qt/map/radar_product_layer.cpp | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 6e315a4a..a78a8c01 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -212,10 +212,10 @@ void RadarProductLayer::UpdateSweep( glEnableVertexAttribArray(0); // Buffer data moments - const GLvoid* data; - GLsizeiptr dataSize; - size_t componentSize; - GLenum type; + const GLvoid* data {}; + GLsizeiptr dataSize {}; + size_t componentSize {}; + GLenum type {}; std::tie(data, dataSize, componentSize) = radarProductView->GetMomentData(); @@ -238,10 +238,10 @@ void RadarProductLayer::UpdateSweep( glEnableVertexAttribArray(1); // Buffer CFP data - const GLvoid* cfpData; - GLsizeiptr cfpDataSize; - size_t cfpComponentSize; - GLenum cfpType; + const GLvoid* cfpData {}; + GLsizeiptr cfpDataSize {}; + size_t cfpComponentSize {}; + GLenum cfpType {}; std::tie(cfpData, cfpDataSize, cfpComponentSize) = radarProductView->GetCfpMomentData(); @@ -313,15 +313,15 @@ void RadarProductLayer::Render( if (sweepVisible) { - const float scale = std::pow(2.0, params.zoom) * 2.0f * - mbgl::util::tileSize_D / mbgl::util::DEGREES_MAX; - const float xScale = scale / params.width; - const float yScale = scale / params.height; + const double scale = std::pow(2.0, params.zoom) * 2.0 * + mbgl::util::tileSize_D / mbgl::util::DEGREES_MAX; + const auto xScale = static_cast(scale / params.width); + const auto yScale = static_cast(scale / params.height); glm::mat4 uMVPMatrix(1.0f); uMVPMatrix = glm::scale(uMVPMatrix, glm::vec3(xScale, yScale, 1.0f)); uMVPMatrix = glm::rotate(uMVPMatrix, - glm::radians(params.bearing), + glm::radians(static_cast(params.bearing)), glm::vec3(0.0f, 0.0f, 1.0f)); glUniform2fv(p->uMapScreenCoordLocation_, @@ -583,7 +583,7 @@ void RadarProductLayer::UpdateColorTable( const uint16_t rangeMin = radarProductView->color_table_min(); const uint16_t rangeMax = radarProductView->color_table_max(); - const float scale = rangeMax - rangeMin; + const auto scale = static_cast(rangeMax - rangeMin); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_1D, p->texture_); From 07adbb382d994c8f133ec205ea0cc5d16eac84a5 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Aug 2025 21:19:33 -0500 Subject: [PATCH 12/37] Prevent flickering of radar data between two "no data available" products --- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 69 ++++++++++++++++--- .../scwx/qt/map/radar_product_layer.cpp | 41 ++++++++--- 2 files changed, 90 insertions(+), 20 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 17374188..185562ad 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -153,6 +153,9 @@ public: float lastFontSize_ {0.0f}; QMargins lastColorTableMargins_ {}; + types::RadarProductLoadStatus latchedLoadStatus_ { + types::RadarProductLoadStatus::ProductNotAvailable}; + double cursorScale_ {1}; boost::signals2::scoped_connection cursorScaleConnection_; @@ -349,12 +352,17 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, auto& settings = mapContext->settings(); const float pixelRatio = mapContext->pixel_ratio(); + types::RadarProductLoadStatus newLoadStatus = + types::RadarProductLoadStatus::ProductNotLoaded; + ImGuiFrameStart(mapContext); p->sweepTimePicked_ = false; if (radarProductView != nullptr) { + newLoadStatus = radarProductView->load_status(); + scwx::util::ClockFormat clockFormat = scwx::util::GetClockFormat( settings::GeneralSettings::Instance().clock_format().GetValue()); @@ -469,6 +477,16 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, ImGuiFrameEnd(); + if (radarProductView != nullptr && + // Don't latch a transition from Not Available to Listing Products + !(p->latchedLoadStatus_ == + types::RadarProductLoadStatus::ProductNotAvailable && + newLoadStatus == types::RadarProductLoadStatus::ListingProducts)) + { + // Latch last load status + p->latchedLoadStatus_ = newLoadStatus; + } + SCWX_GL_CHECK_ERROR(); } @@ -520,11 +538,34 @@ void OverlayLayer::Impl::RenderProductDetails( ImGuiCond_Always, ImVec2 {1.0f, 0.0f}); - if (radarProductView != nullptr && - radarProductView->load_status() == - types::RadarProductLoadStatus::ProductNotAvailable) + bool productNotAvailable = false; + types::RadarProductLoadStatus newLoadStatus = + types::RadarProductLoadStatus::ProductNotLoaded; + + if (radarProductView != nullptr) { - ImGui::Begin("Product Details", + newLoadStatus = radarProductView->load_status(); + + switch (newLoadStatus) + { + case types::RadarProductLoadStatus::ProductNotAvailable: + productNotAvailable = true; + break; + + case types::RadarProductLoadStatus::ListingProducts: + productNotAvailable = + latchedLoadStatus_ == + types::RadarProductLoadStatus::ProductNotAvailable; + break; + + default: + productNotAvailable = false; + } + } + + if (productNotAvailable) + { + ImGui::Begin("Product Not Available", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize); @@ -592,13 +633,19 @@ void OverlayLayer::Impl::RenderAttribution( auto attributionFont = manager::FontManager::Instance().GetImGuiFont( types::FontCategory::Attribution); - ImGui::SetNextWindowPos(ImVec2 {static_cast(params.width), - static_cast(params.height) - - colorTableMargins.bottom()}, - ImGuiCond_Always, - ImVec2 {1.0f, 1.0f}); - ImGui::SetNextWindowBgAlpha(0.5f); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2 {3.0f, 2.0f}); + static constexpr float kWindowBgAlpha_ = 0.5f; + static constexpr float kWindowPaddingX_ = 3.0f; + static constexpr float kWindowPaddingY_ = 2.0f; + + ImGui::SetNextWindowPos( + ImVec2 { + static_cast(params.width), + static_cast(params.height - colorTableMargins.bottom())}, + ImGuiCond_Always, + ImVec2 {1.0f, 1.0f}); + ImGui::SetNextWindowBgAlpha(kWindowBgAlpha_); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, + ImVec2 {kWindowPaddingX_, kWindowPaddingY_}); ImGui::PushFont(attributionFont.first->font(), attributionFont.second.value()); ImGui::Begin("Attribution", diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index a78a8c01..629b84cd 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -64,7 +64,7 @@ public: bool colorTableNeedsUpdate_ {false}; bool sweepNeedsUpdate_ {false}; - types::RadarProductLoadStatus lastLoadStatus_ { + types::RadarProductLoadStatus latchedLoadStatus_ { types::RadarProductLoadStatus::ProductNotAvailable}; }; @@ -161,7 +161,7 @@ void RadarProductLayer::Initialize( } if (reason == types::NoUpdateReason::NoChange) { - if (p->lastLoadStatus_ == + if (p->latchedLoadStatus_ == types::RadarProductLoadStatus::ProductNotAvailable) { // Ensure the radar product is shown by re-rendering @@ -306,10 +306,29 @@ void RadarProductLayer::Render( std::shared_ptr radarProductView = mapContext->radar_product_view(); - const bool sweepVisible = - radarProductView != nullptr && - radarProductView->load_status() != - types::RadarProductLoadStatus::ProductNotAvailable; + bool sweepVisible = false; + types::RadarProductLoadStatus newLoadStatus = + types::RadarProductLoadStatus::ProductNotLoaded; + + if (radarProductView != nullptr) + { + newLoadStatus = radarProductView->load_status(); + + switch (newLoadStatus) + { + case types::RadarProductLoadStatus::ProductNotAvailable: + sweepVisible = false; + break; + + case types::RadarProductLoadStatus::ListingProducts: + sweepVisible = p->latchedLoadStatus_ != + types::RadarProductLoadStatus::ProductNotAvailable; + break; + + default: + sweepVisible = true; + } + } if (sweepVisible) { @@ -350,10 +369,14 @@ void RadarProductLayer::Render( glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } - if (radarProductView != nullptr) + if (radarProductView != nullptr && + // Don't latch a transition from Not Available to Listing Products + !(p->latchedLoadStatus_ == + types::RadarProductLoadStatus::ProductNotAvailable && + newLoadStatus == types::RadarProductLoadStatus::ListingProducts)) { - // Save last load status - p->lastLoadStatus_ = radarProductView->load_status(); + // Latch last load status + p->latchedLoadStatus_ = newLoadStatus; } SCWX_GL_CHECK_ERROR(); From 2beac20ee3d348120213ba3ae6f532e8c3737ae4 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Aug 2025 22:38:31 -0500 Subject: [PATCH 13/37] Asset suffix should be const, not constexpr --- scwx-qt/source/scwx/qt/ui/update_dialog.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp index edb0396e..f3ccb2d8 100644 --- a/scwx-qt/source/scwx/qt/ui/update_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/update_dialog.cpp @@ -93,9 +93,9 @@ void UpdateDialog::Impl::HandleAsset(const types::gh::ReleaseAsset& asset) #if defined(_WIN32) # if defined(_M_AMD64) - static constexpr std::string assetSuffix = "-x64.msi"; + static const std::string assetSuffix = "-x64.msi"; # else - static constexpr std::string assetSuffix = "-arm64.msi"; + static const std::string assetSuffix = "-arm64.msi"; # endif if (asset.name_.ends_with(assetSuffix)) From 403a556b30036d4db3e6fdd5a80ea50d46e83245 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Aug 2025 22:42:56 -0500 Subject: [PATCH 14/37] Add tooltip to NO DATA AVAILABLE indication --- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 185562ad..951ad31a 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -573,6 +573,20 @@ void OverlayLayer::Impl::RenderProductDetails( ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 0, 0, 255)); ImGui::TextUnformatted("NO DATA AVAILABLE"); ImGui::PopStyleColor(); + if (ImGui::BeginItemTooltip()) + { + static constexpr float kFontSizeFactor_ = 20.0f; + static constexpr double kMaxWidthPercent_ = 0.8; + + ImGui::PushTextWrapPos( + std::min(ImGui::GetFontSize() * kFontSizeFactor_, + static_cast(params.width * kMaxWidthPercent_))); + ImGui::TextUnformatted( + "No data found for the selected product and time. Please select a " + "different product, or update your time selection."); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } ImGui::End(); } From b0c7554f47fc7594389d691dbe3d7e8285909991 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Aug 2025 22:41:17 -0500 Subject: [PATCH 15/37] Applying product load status to level 2 --- .../scwx/qt/manager/radar_product_manager.cpp | 104 +++++++++++++++--- .../scwx/qt/view/level2_product_view.cpp | 16 +++ 2 files changed, 107 insertions(+), 13 deletions(-) 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 8806d217..cbd39767 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -203,8 +203,9 @@ public: void RefreshData(std::shared_ptr providerManager); void RefreshDataSync(std::shared_ptr providerManager); - std::map> + std::tuple>, + types::RadarProductLoadStatus> GetLevel2ProductRecords(std::chrono::system_clock::time_point time); std::tuple, std::chrono::system_clock::time_point, @@ -1363,8 +1364,9 @@ void RadarProductManagerImpl::PopulateProductTimes( }); } -std::map> +std::tuple>, + types::RadarProductLoadStatus> RadarProductManagerImpl::GetLevel2ProductRecords( std::chrono::system_clock::time_point time) { @@ -1372,9 +1374,40 @@ RadarProductManagerImpl::GetLevel2ProductRecords( std::shared_ptr> records {}; std::vector recordPtrs {}; + types::RadarProductLoadStatus status { + types::RadarProductLoadStatus::ListingProducts}; + + std::size_t recordPtrCount = 0u; + std::size_t recordCount = 0u; // Ensure Level 2 product records are updated - PopulateLevel2ProductTimes(time); + if (!AreLevel2ProductTimesPopulated(time)) + { + logger_->debug("Level 2 product times need populated: {}", + scwx::util::time::TimeString(time)); + + // Populate level 2 product times asynchronously + boost::asio::post(threadPool_, + [time, this]() + { + // Populate product times + PopulateLevel2ProductTimes(time); + + // Signal finished + Q_EMIT self_->ProductTimesPopulated( + common::RadarProductGroup::Level2, "", time); + }); + + // Return listing products status + return {records, status}; + } + else + { + PopulateLevel2ProductTimes(time, false); + } + + // Advance to loading product + status = types::RadarProductLoadStatus::LoadingProduct; { std::shared_lock lock {level2ProductRecordMutex_}; @@ -1413,9 +1446,29 @@ RadarProductManagerImpl::GetLevel2ProductRecords( if (recordPtr != nullptr) { + using namespace std::chrono_literals; + // Don't check for an exact time match for level 2 products recordTime = recordPtr->first; - record = recordPtr->second.lock(); + + if ( + // For latest data, ensure it is from the last 24 hours + (time == std::chrono::system_clock::time_point {} && + (recordTime > scwx::util::time::now() - 24h || + recordTime == time)) || + // For time queries, ensure data is within 24 hours of the request + (time != std::chrono::system_clock::time_point {} && + std::chrono::abs(recordTime - time) < 24h)) + { + record = recordPtr->second.lock(); + ++recordPtrCount; + } + else + { + // Reset the record + recordPtr = nullptr; + recordTime = time; + } } if (recordPtr != nullptr && record == nullptr && @@ -1438,16 +1491,30 @@ RadarProductManagerImpl::GetLevel2ProductRecords( }); self_->LoadLevel2Data(recordTime, request); + + // Status is already set to LoadingProduct } if (record != nullptr) { // Return valid records records.insert_or_assign(recordTime, record); + ++recordCount; } } - return records; + if (recordPtrCount == 0) + { + // If all records are empty, the product is not available + status = types::RadarProductLoadStatus::ProductNotAvailable; + } + else if (recordCount == recordPtrCount) + { + // If all records were populated, the product has been loaded + status = types::RadarProductLoadStatus::ProductLoaded; + } + + return {records, status}; } std::tuple, @@ -1684,6 +1751,8 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, float elevationCut = 0.0f; std::vector elevationCuts {}; std::chrono::system_clock::time_point foundTime {}; + types::RadarProductLoadStatus loadStatus { + types::RadarProductLoadStatus::ProductNotLoaded}; const bool isEpox = time == std::chrono::system_clock::time_point {}; bool needArchive = true; @@ -1719,6 +1788,7 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, if (foundTime >= firstValidChunkTime) { needArchive = false; + loadStatus = types::RadarProductLoadStatus::ProductLoaded; } } } @@ -1726,7 +1796,11 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, // It is not in the chunk provider, so get it from the archive if (needArchive) { - auto records = p->GetLevel2ProductRecords(time); + std::map> + records; + + std::tie(records, loadStatus) = p->GetLevel2ProductRecords(time); for (auto& recordPair : records) { auto& record = recordPair.second; @@ -1771,11 +1845,15 @@ RadarProductManager::GetLevel2Data(wsr88d::rda::DataBlockType dataBlockType, } } - return {radarData, - elevationCut, - elevationCuts, - foundTime, - types::RadarProductLoadStatus::ProductLoaded}; + if (loadStatus == types::RadarProductLoadStatus::ProductLoaded && + radarData == nullptr) + { + // If all data was available for the time point, but there is no matching + // radar data, consider this as no product available + loadStatus = types::RadarProductLoadStatus::ProductNotAvailable; + } + + return {radarData, elevationCut, elevationCuts, foundTime, loadStatus}; } std::tuple, 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 a9ea3b00..098734ac 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -210,6 +210,22 @@ void Level2ProductView::ConnectRadarProductManager() Update(); } }); + + connect(radar_product_manager().get(), + &manager::RadarProductManager::ProductTimesPopulated, + this, + [this](common::RadarProductGroup group, + const std::string& /* product */, + std::chrono::system_clock::time_point queryTime) + { + if (group == common::RadarProductGroup::Level2 && + queryTime == selected_time()) + { + // If the data associated with the currently selected time is + // reloaded, update the view + Update(); + } + }); } void Level2ProductView::DisconnectRadarProductManager() From 1f8cd8ee39af74f03c8cb5d6c3dd5dd918a56268 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Aug 2025 23:00:59 -0500 Subject: [PATCH 16/37] Overlay layer should not need to latch load status - Causes false NO DATA AVAILABLE indications when changing products --- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 951ad31a..47952eca 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -153,9 +153,6 @@ public: float lastFontSize_ {0.0f}; QMargins lastColorTableMargins_ {}; - types::RadarProductLoadStatus latchedLoadStatus_ { - types::RadarProductLoadStatus::ProductNotAvailable}; - double cursorScale_ {1}; boost::signals2::scoped_connection cursorScaleConnection_; @@ -477,16 +474,6 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, ImGuiFrameEnd(); - if (radarProductView != nullptr && - // Don't latch a transition from Not Available to Listing Products - !(p->latchedLoadStatus_ == - types::RadarProductLoadStatus::ProductNotAvailable && - newLoadStatus == types::RadarProductLoadStatus::ListingProducts)) - { - // Latch last load status - p->latchedLoadStatus_ = newLoadStatus; - } - SCWX_GL_CHECK_ERROR(); } @@ -552,12 +539,6 @@ void OverlayLayer::Impl::RenderProductDetails( productNotAvailable = true; break; - case types::RadarProductLoadStatus::ListingProducts: - productNotAvailable = - latchedLoadStatus_ == - types::RadarProductLoadStatus::ProductNotAvailable; - break; - default: productNotAvailable = false; } From d6834127db61e7dfd80646f0d68eddd9d44c98a2 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 30 Aug 2025 23:10:23 -0500 Subject: [PATCH 17/37] NotAvailable should not hang animation --- scwx-qt/source/scwx/qt/manager/timeline_manager.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp index 6a8765ae..70c4b2ed 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -317,8 +317,7 @@ void TimelineManager::ReceiveRadarSweepNotUpdated(std::size_t mapIndex, types::NoUpdateReason reason) { if (!p->radarSweepMonitorActive_ || - (reason == types::NoUpdateReason::NotLoaded || - reason == types::NoUpdateReason::NotAvailable)) + reason == types::NoUpdateReason::NotLoaded) { return; } From 77ae293e879930f651b1cc255a0de2533e2340a9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 31 Aug 2025 00:02:05 -0500 Subject: [PATCH 18/37] Remove unused newLoadStatus --- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 47952eca..11bbbbea 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -349,17 +349,12 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, auto& settings = mapContext->settings(); const float pixelRatio = mapContext->pixel_ratio(); - types::RadarProductLoadStatus newLoadStatus = - types::RadarProductLoadStatus::ProductNotLoaded; - ImGuiFrameStart(mapContext); p->sweepTimePicked_ = false; if (radarProductView != nullptr) { - newLoadStatus = radarProductView->load_status(); - scwx::util::ClockFormat clockFormat = scwx::util::GetClockFormat( settings::GeneralSettings::Instance().clock_format().GetValue()); From 95b9a034375ebec298b0cf24a14253fb522a44fc Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 31 Aug 2025 00:02:35 -0500 Subject: [PATCH 19/37] Add required parameter for GetTimePointsByDate --- test/source/scwx/provider/aws_level3_data_provider.test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/source/scwx/provider/aws_level3_data_provider.test.cpp b/test/source/scwx/provider/aws_level3_data_provider.test.cpp index 31ea50ec..6e9f3bb2 100644 --- a/test/source/scwx/provider/aws_level3_data_provider.test.cpp +++ b/test/source/scwx/provider/aws_level3_data_provider.test.cpp @@ -76,7 +76,7 @@ TEST(AwsLevel3DataProvider, GetTimePointsByDate) AwsLevel3DataProvider provider("KLSX", "N0Q"); - auto timePoints = provider.GetTimePointsByDate(date); + auto timePoints = provider.GetTimePointsByDate(date, true); EXPECT_GT(timePoints.size(), 0); for (auto timePoint : timePoints) From 22a6ed33c1c907df322e1799bc2e7ed4b44e5426 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 31 Aug 2025 00:44:26 -0500 Subject: [PATCH 20/37] Product load status clang-tidy fixes --- scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 15 +++++++++++---- .../source/scwx/qt/map/radar_product_layer.cpp | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 11bbbbea..3c942b28 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -438,9 +438,13 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, // Map Center Icon if (params.width != p->lastWidth_ || params.height != p->lastHeight_) { + static constexpr double xPosition = 0.5; + static constexpr double yPosition = 0.5; + // Draw the icon in the center of the widget - p->icons_->SetIconLocation( - p->mapCenterIcon_, params.width / 2.0, params.height / 2.0); + p->icons_->SetIconLocation(p->mapCenterIcon_, + params.width * xPosition, + params.height * yPosition); } p->icons_->SetIconVisible(p->mapCenterIcon_, generalSettings.show_map_center().GetValue()); @@ -448,10 +452,13 @@ void OverlayLayer::Render(const std::shared_ptr& mapContext, const QMargins colorTableMargins = mapContext->color_table_margins(); if (colorTableMargins != p->lastColorTableMargins_ || p->firstRender_) { + static constexpr int xOffset = 10; + static constexpr int yOffset = 10; + // Draw map logo with a 10x10 indent from the bottom left p->icons_->SetIconLocation(p->mapLogoIcon_, - 10 + colorTableMargins.left(), - 10 + colorTableMargins.bottom()); + colorTableMargins.left() + xOffset, + colorTableMargins.bottom() + yOffset); } p->icons_->SetIconVisible(p->mapLogoIcon_, generalSettings.show_map_logo().GetValue()); diff --git a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp index 629b84cd..38eeff41 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -303,7 +303,7 @@ void RadarProductLayer::Render( UpdateSweep(mapContext); } - std::shared_ptr radarProductView = + const std::shared_ptr radarProductView = mapContext->radar_product_view(); bool sweepVisible = false; From acfb515e10ab4481c6027fcbc4751099940b853d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 31 Aug 2025 10:32:04 -0500 Subject: [PATCH 21/37] Filter PopulateProductTimes log entries --- .../scwx/qt/manager/radar_product_manager.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) 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 cbd39767..dce5c8ea 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -1302,11 +1302,20 @@ void RadarProductManagerImpl::PopulateProductTimes( std::chrono::system_clock::time_point time, bool update) { - logger_->debug("Populating product times (Update: {}): {}, {}, {}", - update, - common::GetRadarProductGroupName(providerManager->group_), - providerManager->product_, - scwx::util::time::TimeString(time)); + if (update) + { + logger_->debug("Populating product times: {}, {}, {}", + common::GetRadarProductGroupName(providerManager->group_), + providerManager->product_, + scwx::util::time::TimeString(time)); + } + else + { + logger_->trace("Populating cached product times: {}, {}, {}", + common::GetRadarProductGroupName(providerManager->group_), + providerManager->product_, + scwx::util::time::TimeString(time)); + } auto today = std::chrono::floor(time); From 889b6e81becfbd9d02d12aba5e3ec6ad336c9670 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 1 Sep 2025 01:30:47 -0500 Subject: [PATCH 22/37] Taking load data mutexes on destruction can cause a deadlock --- scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp | 4 ---- 1 file changed, 4 deletions(-) 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 dce5c8ea..b3754f5f 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -182,10 +182,6 @@ public: providerManager->Disable(); }); - // Lock other mutexes before destroying, ensure loading is complete - std::unique_lock loadLevel2DataLock {loadLevel2DataMutex_}; - std::unique_lock loadLevel3DataLock {loadLevel3DataMutex_}; - threadPool_.join(); } From 7fbd9e45a9d3a24b9b8f35e1b0eb4da9ecfe0318 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 1 Sep 2025 09:44:37 -0500 Subject: [PATCH 23/37] Release all mutexes before joining threads in RadarProductManager --- scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp | 1 + 1 file changed, 1 insertion(+) 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 b3754f5f..6229bae1 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -181,6 +181,7 @@ public: auto& [key, providerManager] = p; providerManager->Disable(); }); + lock.unlock(); threadPool_.join(); } From 985473a0a4eb92001c55d78b1a04aabf09fccf7e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 1 Sep 2025 10:00:48 -0500 Subject: [PATCH 24/37] Release all mutexes before joining threads in TimelineManager --- scwx-qt/source/scwx/qt/manager/timeline_manager.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp index 70c4b2ed..46e827ea 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -53,8 +53,10 @@ public: // Lock mutexes before destroying std::unique_lock animationTimerLock {animationTimerMutex_}; animationTimer_.cancel(); + animationTimerLock.unlock(); - std::unique_lock selectTimeLock {selectTimeMutex_}; + selectThreadPool_.join(); + playThreadPool_.join(); } TimelineManager* self_; From 3fe7dd9eed4ecb5be17d881709f31fabcf56c5da Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 3 Sep 2025 22:16:22 -0500 Subject: [PATCH 25/37] RefreshData should be run entirely through ProviderManager - Impl may be destroyed before ProviderManager, leading to a use-after-free condition (lifetime race condition) - Moving to ProviderManager entirely should let the thread pool join prevent use-after-free (cherry picked from commit 212cca973f6f5d1462e19a8a3e1cc535e691e552) --- .../scwx/qt/manager/radar_product_manager.cpp | 86 +++++++++---------- 1 file changed, 42 insertions(+), 44 deletions(-) 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 6229bae1..ce56a165 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -106,21 +106,27 @@ public: group, product, isChunks_, latestTime); }); } - ~ProviderManager() { threadPool_.join(); }; + ~ProviderManager() + { + providerThreadPool_.stop(); + providerThreadPool_.join(); + }; std::string name() const; void Disable(); + void RefreshData(); + void RefreshDataSync(); - boost::asio::thread_pool threadPool_ {1u}; + boost::asio::thread_pool providerThreadPool_ {1u}; - const std::string radarId_; - const common::RadarProductGroup group_; - const std::string product_; - const bool isChunks_; - bool refreshEnabled_ {false}; - boost::asio::steady_timer refreshTimer_ {threadPool_}; - std::mutex refreshTimerMutex_ {}; + const std::string radarId_; + const common::RadarProductGroup group_; + const std::string product_; + const bool isChunks_; + bool refreshEnabled_ {false}; + boost::asio::steady_timer refreshTimer_ {providerThreadPool_}; + std::mutex refreshTimerMutex_ {}; std::shared_ptr provider_ {nullptr}; size_t refreshCount_ {0}; @@ -183,6 +189,7 @@ public: }); lock.unlock(); + threadPool_.stop(); threadPool_.join(); } @@ -197,8 +204,6 @@ public: boost::uuids::uuid uuid, const std::set>& providerManagers, bool enabled); - void RefreshData(std::shared_ptr providerManager); - void RefreshDataSync(std::shared_ptr providerManager); std::tuple>, @@ -781,28 +786,27 @@ void RadarProductManagerImpl::EnableRefresh( if (providerManager->refreshEnabled_ != enabled) { providerManager->refreshEnabled_ = enabled; - RefreshData(providerManager); + providerManager->RefreshData(); } } } } -void RadarProductManagerImpl::RefreshData( - std::shared_ptr providerManager) +void ProviderManager::RefreshData() { - logger_->trace("RefreshData: {}", providerManager->name()); + logger_->trace("RefreshData: {}", name()); { - std::unique_lock lock(providerManager->refreshTimerMutex_); - providerManager->refreshTimer_.cancel(); + std::unique_lock lock(refreshTimerMutex_); + refreshTimer_.cancel(); } - boost::asio::post(threadPool_, + boost::asio::post(providerThreadPool_, [=, this]() { try { - RefreshDataSync(providerManager); + RefreshDataSync(); } catch (const std::exception& ex) { @@ -811,27 +815,24 @@ void RadarProductManagerImpl::RefreshData( }); } -void RadarProductManagerImpl::RefreshDataSync( - std::shared_ptr providerManager) +void ProviderManager::RefreshDataSync() { using namespace std::chrono_literals; - auto [newObjects, totalObjects] = providerManager->provider_->Refresh(); + auto [newObjects, totalObjects] = provider_->Refresh(); // Level2 chunked data is updated quickly and uses a faster interval const std::chrono::milliseconds fastRetryInterval = - providerManager->isChunks_ ? kFastRetryIntervalChunks_ : - kFastRetryInterval_; + isChunks_ ? kFastRetryIntervalChunks_ : kFastRetryInterval_; const std::chrono::milliseconds slowRetryInterval = - providerManager->isChunks_ ? kSlowRetryIntervalChunks_ : - kSlowRetryInterval_; + isChunks_ ? kSlowRetryIntervalChunks_ : kSlowRetryInterval_; std::chrono::milliseconds interval = fastRetryInterval; if (totalObjects > 0) { - auto latestTime = providerManager->provider_->FindLatestTime(); - auto updatePeriod = providerManager->provider_->update_period(); - auto lastModified = providerManager->provider_->last_modified(); + auto latestTime = provider_->FindLatestTime(); + auto updatePeriod = provider_->update_period(); + auto lastModified = provider_->last_modified(); auto sinceLastModified = scwx::util::time::now() - lastModified; // For the default interval, assume products are updated at a @@ -854,46 +855,43 @@ void RadarProductManagerImpl::RefreshDataSync( if (newObjects > 0) { - Q_EMIT providerManager->NewDataAvailable( - providerManager->group_, providerManager->product_, latestTime); + Q_EMIT NewDataAvailable(group_, product_, latestTime); } } - else if (providerManager->refreshEnabled_) + else if (refreshEnabled_) { - logger_->info("[{}] No data found", providerManager->name()); + logger_->info("[{}] No data found", name()); // If no data is found, retry at the slow retry interval interval = slowRetryInterval; } - std::unique_lock const lock(providerManager->refreshTimerMutex_); + std::unique_lock const lock(refreshTimerMutex_); - if (providerManager->refreshEnabled_) + if (refreshEnabled_) { logger_->trace( "[{}] Scheduled refresh in {:%M:%S}", - providerManager->name(), + name(), std::chrono::duration_cast(interval)); { - providerManager->refreshTimer_.expires_after(interval); - providerManager->refreshTimer_.async_wait( + refreshTimer_.expires_after(interval); + refreshTimer_.async_wait( [=, this](const boost::system::error_code& e) { if (e == boost::system::errc::success) { - RefreshData(providerManager); + RefreshData(); } else if (e == boost::asio::error::operation_aborted) { - logger_->debug("[{}] Data refresh timer cancelled", - providerManager->name()); + logger_->debug("[{}] Data refresh timer cancelled", name()); } else { - logger_->warn("[{}] Data refresh timer error: {}", - providerManager->name(), - e.message()); + logger_->warn( + "[{}] Data refresh timer error: {}", name(), e.message()); } }); } From f34d11a7ea592b865b2a03fc6d37dc1da96a7909 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 3 Sep 2025 22:38:56 -0500 Subject: [PATCH 26/37] RefreshData clang-tidy fixes (cherry picked from commit c630c2969929ce46d96fd4f941f054dcc70d1d8b) --- .../scwx/qt/manager/radar_product_manager.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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 ce56a165..feda8a7d 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -106,7 +106,7 @@ public: group, product, isChunks_, latestTime); }); } - ~ProviderManager() + ~ProviderManager() override { providerThreadPool_.stop(); providerThreadPool_.join(); @@ -797,12 +797,12 @@ void ProviderManager::RefreshData() logger_->trace("RefreshData: {}", name()); { - std::unique_lock lock(refreshTimerMutex_); + const std::unique_lock lock(refreshTimerMutex_); refreshTimer_.cancel(); } boost::asio::post(providerThreadPool_, - [=, this]() + [this]() { try { @@ -841,7 +841,11 @@ void ProviderManager::RefreshDataSync() interval = std::chrono::duration_cast( updatePeriod - sinceLastModified); - if (updatePeriod > 0s && sinceLastModified > updatePeriod * 5) + // Allow 5 update periods before considering the data stale + constexpr std::size_t kUpdatePeriodStaleCount = 5; + + if (updatePeriod > 0s && + sinceLastModified > updatePeriod * kUpdatePeriodStaleCount) { // If it has been at least 5 update periods since the file has // been last modified, slow the retry period @@ -878,7 +882,7 @@ void ProviderManager::RefreshDataSync() { refreshTimer_.expires_after(interval); refreshTimer_.async_wait( - [=, this](const boost::system::error_code& e) + [this](const boost::system::error_code& e) { if (e == boost::system::errc::success) { From 76a74922c5cce597852f920416034ce83b17436a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 4 Sep 2025 21:34:21 -0500 Subject: [PATCH 27/37] AlertLayer threading fixes (cherry picked from commit e6c395a657bbb6d3532eb0cf883c154a3b508deb) --- scwx-qt/source/scwx/qt/map/alert_layer.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/alert_layer.cpp b/scwx-qt/source/scwx/qt/map/alert_layer.cpp index 7bea5938..4429eeb8 100644 --- a/scwx-qt/source/scwx/qt/map/alert_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/alert_layer.cpp @@ -6,8 +6,8 @@ #include #include #include -#include +#include #include #include #include @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -22,7 +23,6 @@ #include #include #include -#include namespace scwx::qt::map { @@ -153,9 +153,11 @@ public: ~Impl() { std::unique_lock refreshLock(refreshMutex_); + refreshEnabled_ = false; refreshTimer_.cancel(); refreshLock.unlock(); + threadPool_.stop(); threadPool_.join(); receiver_ = nullptr; @@ -213,6 +215,7 @@ public: AlertLayer* self_; + std::atomic refreshEnabled_ {true}; boost::asio::system_timer refreshTimer_ {threadPool_}; std::mutex refreshMutex_; @@ -582,7 +585,8 @@ void AlertLayer::Impl::ScheduleRefresh() // Expires at the top of the next minute std::chrono::system_clock::time_point now = - std::chrono::floor(scwx::util::time::now()); + std::chrono::floor( + std::chrono::system_clock::now()); refreshTimer_.expires_at(now + 1min); refreshTimer_.async_wait( @@ -599,7 +603,11 @@ void AlertLayer::Impl::ScheduleRefresh() else { Q_EMIT self_->NeedsRendering(); - ScheduleRefresh(); + + if (refreshEnabled_) + { + ScheduleRefresh(); + } } }); } From cb749e7b9ef32586dd82ea7da8a4b89fa720077f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 6 Sep 2025 08:54:17 -0500 Subject: [PATCH 28/37] Re-add chunks provider to populate product times --- scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp | 5 +++++ 1 file changed, 5 insertions(+) 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 feda8a7d..bfef8e24 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -1272,6 +1272,11 @@ void RadarProductManagerImpl::PopulateLevel2ProductTimes( level2ProductRecordMutex_, time, update); + PopulateProductTimes(level2ChunksProviderManager_, + level2ProductRecords_, + level2ProductRecordMutex_, + time, + update); } void RadarProductManagerImpl::PopulateLevel3ProductTimes( From 9416ff154645a06593cb3411b13edde0d7888040 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 6 Sep 2025 20:40:58 -0500 Subject: [PATCH 29/37] Level 2 bucket name changed The NEXRAD Level II archive data is moving to a new bucket: unidata-nexrad-level2 and SNS topic: arn:aws:sns:us-east-1:684042711724:NewNEXRADLevel2Archive. The old bucket and SNS topic are now deprecated and will no longer be available starting September 1, 2025. --- wxdata/source/scwx/provider/aws_level2_data_provider.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wxdata/source/scwx/provider/aws_level2_data_provider.cpp b/wxdata/source/scwx/provider/aws_level2_data_provider.cpp index c2910773..074f61f1 100644 --- a/wxdata/source/scwx/provider/aws_level2_data_provider.cpp +++ b/wxdata/source/scwx/provider/aws_level2_data_provider.cpp @@ -18,7 +18,7 @@ static const std::string logPrefix_ = "scwx::provider::aws_level2_data_provider"; static const auto logger_ = util::Logger::Create(logPrefix_); -static const std::string kDefaultBucketName_ = "noaa-nexrad-level2"; +static const std::string kDefaultBucketName_ = "unidata-nexrad-level2"; static const std::string kDefaultRegion_ = "us-east-1"; class AwsLevel2DataProvider::Impl From a952d890e6e2189421e1a005790ea544233c964d Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 6 Sep 2025 23:08:56 -0500 Subject: [PATCH 30/37] Ensure animation thread pools stop instead of taking on new work on destruction --- scwx-qt/source/scwx/qt/manager/timeline_manager.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp index 46e827ea..c4b9f2e3 100644 --- a/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/timeline_manager.cpp @@ -55,6 +55,9 @@ public: animationTimer_.cancel(); animationTimerLock.unlock(); + selectThreadPool_.stop(); + playThreadPool_.stop(); + selectThreadPool_.join(); playThreadPool_.join(); } From a43c2df13fe57899c7eea9e91d63e487388a051a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 6 Sep 2025 23:09:15 -0500 Subject: [PATCH 31/37] Add an additional provider thread pool --- scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 bfef8e24..048e2542 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -118,7 +118,7 @@ public: void RefreshData(); void RefreshDataSync(); - boost::asio::thread_pool providerThreadPool_ {1u}; + boost::asio::thread_pool providerThreadPool_ {2u}; const std::string radarId_; const common::RadarProductGroup group_; From 2dc69544eca1e9f2d140c0fe6ca993b6772091d8 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 7 Sep 2025 08:48:44 -0500 Subject: [PATCH 32/37] Fix Mineral Mapbox style drawBelow value to tunnel --- scwx-qt/source/scwx/qt/map/map_provider.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/map/map_provider.cpp b/scwx-qt/source/scwx/qt/map/map_provider.cpp index b1b5979d..81d21a66 100644 --- a/scwx-qt/source/scwx/qt/map/map_provider.cpp +++ b/scwx-qt/source/scwx/qt/map/map_provider.cpp @@ -73,7 +73,7 @@ static const std::unordered_map mapProviderInfo_ { .drawBelow_ {mapboxDrawBelow_}}, {.name_ {"Mineral"}, .url_ {"mapbox://styles/mapbox/cjtep62gq54l21frr1whf27ak"}, - .drawBelow_ {mapboxDrawBelow_}}, + .drawBelow_ {"tunnel"}}, {.name_ {"Minimo"}, .url_ { "mapbox://styles/mapbox-map-design/cksjc2nsq1bg117pnekb655h1"}, From 94dc5b547d190a1f0ba432e1af759a0bb7b59480 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 7 Sep 2025 08:49:13 -0500 Subject: [PATCH 33/37] Bump version to v0.5.2 --- .github/workflows/ci.yml | 2 +- CMakeLists.txt | 4 ++-- tools/net.supercellwx.app.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3033807..f6f5ed2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,7 +127,7 @@ jobs: env: CC: ${{ matrix.env_cc }} CXX: ${{ matrix.env_cxx }} - SCWX_VERSION: v0.5.1 + SCWX_VERSION: v0.5.2 runs-on: ${{ matrix.os }} steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index e76cb8d1..9b33b070 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_OSX_DEPLOYMENT_TARGET 12.0) scwx_python_setup() project(${PROJECT_NAME} - VERSION 0.5.1 + VERSION 0.5.2 DESCRIPTION "Supercell Wx is a free, open source advanced weather radar viewer." HOMEPAGE_URL "https://github.com/dpaulat/supercell-wx" LANGUAGES C CXX) @@ -32,7 +32,7 @@ set_property(DIRECTORY set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBOOST_ALL_NO_LIB") set(SCWX_DIR ${PROJECT_SOURCE_DIR}) -set(SCWX_VERSION "0.5.1") +set(SCWX_VERSION "0.5.2") option(SCWX_ADDRESS_SANITIZER "Build with Address Sanitizer" OFF) diff --git a/tools/net.supercellwx.app.yml b/tools/net.supercellwx.app.yml index 762edf28..9c1119ce 100644 --- a/tools/net.supercellwx.app.yml +++ b/tools/net.supercellwx.app.yml @@ -1,5 +1,5 @@ id: net.supercellwx.app -version: '0.5.1' +version: '0.5.2' runtime: "org.freedesktop.Platform" runtime-version: "23.08" sdk: "org.freedesktop.Sdk" From 54fd86de5c2ee380b18fb421d8157ba673730e91 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 14 Sep 2025 11:48:25 -0500 Subject: [PATCH 34/37] Ensure level 3 product is validated in settings to prevent crash on invalid value --- scwx-qt/source/scwx/qt/settings/map_settings.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.cpp b/scwx-qt/source/scwx/qt/settings/map_settings.cpp index 76c09d30..675071b9 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.cpp @@ -86,8 +86,9 @@ public: } else { - // TODO: Validate level 3 product - return true; + // Validate level 3 product + auto level3Product = common::GetLevel3ProductByAwipsId(value); + return !level3Product.empty() && level3Product != "?"; } }); From d687d04d7601a1d67dc1c5626525936a6a354938 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 14 Sep 2025 13:59:24 -0500 Subject: [PATCH 35/37] Make sure level3Product is const --- scwx-qt/source/scwx/qt/settings/map_settings.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.cpp b/scwx-qt/source/scwx/qt/settings/map_settings.cpp index 675071b9..74b156b5 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.cpp @@ -87,7 +87,8 @@ public: else { // Validate level 3 product - auto level3Product = common::GetLevel3ProductByAwipsId(value); + const auto level3Product = + common::GetLevel3ProductByAwipsId(value); return !level3Product.empty() && level3Product != "?"; } }); From eed2736ddee6db98d8f0169cc15fb245e5c39961 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 14 Sep 2025 14:00:27 -0500 Subject: [PATCH 36/37] Update level 3 defaults in test data --- test/data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/data b/test/data index c68bee74..fd8bc8bf 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit c68bee74549963e9a02e0fa998efad0f10f8256b +Subproject commit fd8bc8bf1d07474886ce6773abffab4d315d0cd0 From 2cc5e4810283baa1832ea9c83cfc519ad3234216 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 20 Sep 2025 08:23:26 -0500 Subject: [PATCH 37/37] Elevation cuts needs protected by mutex - If elevation cuts is read while GetLevel2Data is being called from ComputeSweep, the application will crash. - Additionally, the elevationCut_ variable should probably be made atomic. --- .../source/scwx/qt/view/level2_product_view.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 098734ac..a0805bc7 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -8,6 +8,9 @@ #include #include +#include +#include + #include #include @@ -160,11 +163,13 @@ public: float latitude_; float longitude_; - float elevationCut_; + std::atomic elevationCut_; std::vector elevationCuts_; units::kilometers range_; uint16_t vcp_; + std::mutex elevationCutsMutex_ {}; + std::chrono::system_clock::time_point sweepTime_; std::shared_ptr colorTable_; @@ -368,6 +373,7 @@ std::string Level2ProductView::GetRadarProductName() const std::vector Level2ProductView::GetElevationCuts() const { + const std::unique_lock lock {p->elevationCutsMutex_}; return p->elevationCuts_; } @@ -565,11 +571,17 @@ void Level2ProductView::ComputeSweep() std::shared_ptr radarData; std::chrono::system_clock::time_point requestedTime {selected_time()}; types::RadarProductLoadStatus loadStatus {}; + + std::vector newElevationCuts {}; std::tie( - radarData, p->elevationCut_, p->elevationCuts_, std::ignore, loadStatus) = + radarData, p->elevationCut_, newElevationCuts, std::ignore, loadStatus) = radarProductManager->GetLevel2Data( p->dataBlockType_, p->selectedElevation_, requestedTime); + std::unique_lock elevationCutsLock {p->elevationCutsMutex_}; + p->elevationCuts_ = newElevationCuts; + elevationCutsLock.unlock(); + set_load_status(loadStatus); if (radarData == nullptr)