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 9768c9da..48584a2d 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -230,6 +230,7 @@ public: const std::string radarId_; bool initialized_; bool level3ProductsInitialized_; + bool level3AvailabilityReady_ {false}; std::shared_ptr radarSite_; std::size_t cacheLimit_ {6u}; @@ -428,9 +429,16 @@ const scwx::util::time_zone* RadarProductManager::default_time_zone() const } } +bool RadarProductManager::is_tdwr() const +{ + return p->radarSite_->type() == "tdwr"; +} + float RadarProductManager::gate_size() const { - return (p->radarSite_->type() == "tdwr") ? 150.0f : 250.0f; + // tdwr is 150 meter per gate, wsr88d is 250 meter per gate + // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers) + return (is_tdwr()) ? 150.0f : 250.0f; } std::string RadarProductManager::radar_id() const @@ -454,6 +462,12 @@ void RadarProductManager::Initialize() logger_->debug("Initialize()"); + if (is_tdwr()) + { + p->initialized_ = true; + return; + } + boost::timer::cpu_timer timer; // Calculate half degree azimuth coordinates @@ -1572,6 +1586,12 @@ void RadarProductManager::UpdateAvailableProducts() if (p->level3ProductsInitialized_) { + if (p->level3AvailabilityReady_) + { + // Multiple maps may use the same manager, so this ensures that all get + // notified of the change + Q_EMIT Level3ProductsChanged(); + } return; } @@ -1647,6 +1667,7 @@ void RadarProductManagerImpl::UpdateAvailableProductsSync() } } + level3AvailabilityReady_ = true; Q_EMIT self_->Level3ProductsChanged(); } 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 3f4899ea..ee54f147 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp @@ -44,6 +44,7 @@ public: [[nodiscard]] const std::vector& coordinates(common::RadialSize radialSize, bool smoothingEnabled) const; [[nodiscard]] const scwx::util::time_zone* default_time_zone() const; + [[nodiscard]] bool is_tdwr() const; [[nodiscard]] float gate_size() const; [[nodiscard]] std::string radar_id() const; [[nodiscard]] std::shared_ptr radar_site() const; diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index f2599579..b4882345 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -31,6 +31,7 @@ #include #include +#include #include #include @@ -178,9 +179,11 @@ public: void SelectNearestRadarSite(double latitude, double longitude, std::optional type); - void SetRadarSite(const std::string& radarSite); + void SetRadarSite(const std::string& radarSite, + bool checkProductAvailability = false); void UpdateLoadedStyle(); bool UpdateStoredMapParameters(); + void CheckLevel3Availability(); std::string FindMapSymbologyLayer(); @@ -268,6 +271,10 @@ public: std::set activeHotkeys_ {}; std::chrono::system_clock::time_point prevHotkeyTime_ {}; + bool productAvailabilityCheckNeeded_ {false}; + bool productAvailabilityUpdated_ {false}; + bool productAvailabilityProductSelected_ {false}; + public slots: void Update(); }; @@ -429,6 +436,14 @@ void MapWidgetImpl::ConnectSignals() &manager::HotkeyManager::HotkeyReleased, this, &MapWidgetImpl::HandleHotkeyReleased); + connect(widget_, + &MapWidget::RadarSiteUpdated, + widget_, + [this](const std::shared_ptr&) + { + productAvailabilityProductSelected_ = true; + CheckLevel3Availability(); + }); } void MapWidgetImpl::HandleHotkeyPressed(types::Hotkey hotkey, bool isAutoRepeat) @@ -913,7 +928,7 @@ void MapWidget::SelectRadarSite(std::shared_ptr radarSite, p->map_->setCoordinate( {radarSite->latitude(), radarSite->longitude()}); } - p->SetRadarSite(radarSite->id()); + p->SetRadarSite(radarSite->id(), true); p->Update(); // Select products from new site @@ -1772,7 +1787,12 @@ void MapWidgetImpl::RadarProductManagerConnect() connect(radarProductManager_.get(), &manager::RadarProductManager::Level3ProductsChanged, this, - [this]() { Q_EMIT widget_->Level3ProductsChanged(); }); + [this]() + { + productAvailabilityUpdated_ = true; + CheckLevel3Availability(); + Q_EMIT widget_->Level3ProductsChanged(); + }); connect( radarProductManager_.get(), @@ -1990,7 +2010,8 @@ void MapWidgetImpl::SelectNearestRadarSite(double latitude, } } -void MapWidgetImpl::SetRadarSite(const std::string& radarSite) +void MapWidgetImpl::SetRadarSite(const std::string& radarSite, + bool checkProductAvailability) { // Check if radar site has changed if (radarProductManager_ == nullptr || @@ -2009,6 +2030,12 @@ void MapWidgetImpl::SetRadarSite(const std::string& radarSite) // Connect signals to new RadarProductManager RadarProductManagerConnect(); + // Once the available products are loaded, check to make sure the current + // one is available + productAvailabilityCheckNeeded_ = checkProductAvailability; + productAvailabilityUpdated_ = false; + productAvailabilityProductSelected_ = false; + radarProductManager_->UpdateAvailableProducts(); } } @@ -2053,6 +2080,83 @@ bool MapWidgetImpl::UpdateStoredMapParameters() return changed; } +void MapWidgetImpl::CheckLevel3Availability() +{ + /* + * productAvailabilityCheckNeeded_ Only do this when it is indicated that it + * is needed (mostly on radar site change). This is mainly to avoid potential + * recursion with SelectRadarProduct calls. + * + * productAvailabilityUpdated_ Only update once the product availability + * has been updated + * + * productAvailabilityProductSelected_ Only update once the radar site is + * fully selected, including the current product + */ + if (!(productAvailabilityCheckNeeded_ && productAvailabilityUpdated_ && + productAvailabilityProductSelected_)) + { + return; + } + productAvailabilityCheckNeeded_ = false; + + // Only do this for level3 products + if (widget_->GetRadarProductGroup() != common::RadarProductGroup::Level3) + { + return; + } + + const common::Level3ProductCategoryMap& categoryMap = + widget_->GetAvailableLevel3Categories(); + + const std::string& productTilt = context_->radar_product(); + const std::string& productName = + common::GetLevel3ProductByAwipsId(productTilt); + const common::Level3ProductCategory productCategory = + common::GetLevel3CategoryByProduct(productName); + if (productCategory == common::Level3ProductCategory::Unknown) + { + return; + } + + const auto& availableProductsIt = categoryMap.find(productCategory); + // Has no products in this category, do not change categories + if (availableProductsIt == categoryMap.cend()) + { + return; + } + + const auto& availableProducts = availableProductsIt->second; + const auto& availableTiltsIt = availableProducts.find(productName); + // Does not have the same product, but has others in the same category. + // Switch to the default product and tilt in this category. + if (availableTiltsIt == availableProducts.cend()) + { + widget_->SelectRadarProduct( + common::RadarProductGroup::Level3, + common::GetLevel3CategoryDefaultProduct(productCategory, categoryMap), + 0, + widget_->GetSelectedTime()); + return; + } + + const auto& availableTilts = availableTiltsIt->second; + const auto& tilt = std::ranges::find_if( + availableTilts, + [productTilt](const std::string& tilt) { return productTilt == tilt; }); + // Tilt is not available, set it to first tilt + if (tilt == availableTilts.cend() && availableTilts.size() > 0) + { + widget_->SelectRadarProduct(common::RadarProductGroup::Level3, + availableTilts[0], + 0, + widget_->GetSelectedTime()); + return; + } + + // Tilt is available, no change needed +} + } // namespace map } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp index 98f091ac..e6de32b2 100644 --- a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp @@ -60,7 +60,9 @@ public: categoryButtons_ {}, productTiltMap_ {}, awipsProductMap_ {}, - awipsProductMutex_ {} + awipsProductMutex_ {}, + categoryMap_ {}, + categoryMapMutex_ {} { layout_->setContentsMargins(0, 0, 0, 0); layout_->addWidget(productsWidget_); @@ -183,6 +185,9 @@ public: std::unordered_map awipsProductMap_; std::shared_mutex awipsProductMutex_; + common::Level3ProductCategoryMap categoryMap_; + std::shared_mutex categoryMapMutex_; + std::string currentAwipsId_ {}; QAction* currentProductTiltAction_ {nullptr}; @@ -322,9 +327,11 @@ void Level3ProductsWidgetImpl::SelectProductCategory( { UpdateCategorySelection(category); + const std::shared_lock lock {categoryMapMutex_}; + Q_EMIT self_->RadarProductSelected( common::RadarProductGroup::Level3, - common::GetLevel3CategoryDefaultProduct(category), + common::GetLevel3CategoryDefaultProduct(category, categoryMap_), 0); } @@ -333,6 +340,12 @@ void Level3ProductsWidget::UpdateAvailableProducts( { logger_->trace("UpdateAvailableProducts()"); + // Save the category map + { + const std::unique_lock lock {p->categoryMapMutex_}; + p->categoryMap_ = updatedCategoryMap; + } + // Iterate through each category tool button std::for_each( p->categoryButtons_.cbegin(), 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 135f5e65..0b3f42fa 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -39,13 +39,13 @@ public: vcp_ {}, sweepTime_ {} { - coordinates_.resize(kMaxCoordinates_); } ~Impl() { threadPool_.join(); }; void ComputeCoordinates( const std::shared_ptr& radialData, - bool smoothingEnabled); + bool smoothingEnabled, + float gateSize); [[nodiscard]] inline std::uint8_t RemapDataMoment(std::uint8_t dataMoment) const; @@ -269,17 +269,24 @@ void Level3RadialView::ComputeSweep() } common::RadialSize radialSize; - if (radials == common::MAX_0_5_DEGREE_RADIALS) + if (radarProductManager->is_tdwr()) { - radialSize = common::RadialSize::_0_5Degree; - } - else if (radials == common::MAX_1_DEGREE_RADIALS) - { - radialSize = common::RadialSize::_1Degree; + radialSize = common::RadialSize::NonStandard; } else { - radialSize = common::RadialSize::NonStandard; + if (radials == common::MAX_0_5_DEGREE_RADIALS) + { + radialSize = common::RadialSize::_0_5Degree; + } + else if (radials == common::MAX_1_DEGREE_RADIALS) + { + radialSize = common::RadialSize::_1Degree; + } + else + { + radialSize = common::RadialSize::NonStandard; + } } const std::vector& coordinates = @@ -323,11 +330,21 @@ void Level3RadialView::ComputeSweep() // Compute threshold at which to display an individual bin const uint16_t snrThreshold = descriptionBlock->threshold(); + // Compute gate interval + const std::uint16_t dataMomentInterval = + descriptionBlock->x_resolution_raw(); + + // Get the gate length in meters. Use dataMomentInterval for NonStandard to + // avoid generating >1 base gates per bin. + const float gateLength = radialSize == common::RadialSize::NonStandard ? + static_cast(dataMomentInterval) : + radarProductManager->gate_size(); + // Determine which radial to start at std::uint16_t startRadial; if (radialSize == common::RadialSize::NonStandard) { - p->ComputeCoordinates(radialData, smoothingEnabled); + p->ComputeCoordinates(radialData, smoothingEnabled, gateLength); startRadial = 0; } else @@ -337,15 +354,9 @@ void Level3RadialView::ComputeSweep() startRadial = std::lroundf(startAngle * radialMultiplier); } - // Compute gate interval - const std::uint16_t dataMomentInterval = - descriptionBlock->x_resolution_raw(); - // Compute gate size (number of base gates per bin) const std::uint16_t gateSize = std::max( - 1, - dataMomentInterval / - static_cast(radarProductManager->gate_size())); + 1, dataMomentInterval / static_cast(gateLength)); // Compute gate range [startGate, endGate) std::uint16_t startGate = 0; @@ -526,7 +537,8 @@ Level3RadialView::Impl::RemapDataMoment(std::uint8_t dataMoment) const void Level3RadialView::Impl::ComputeCoordinates( const std::shared_ptr& radialData, - bool smoothingEnabled) + bool smoothingEnabled, + float gateSize) { logger_->debug("ComputeCoordinates()"); @@ -537,13 +549,14 @@ void Level3RadialView::Impl::ComputeCoordinates( auto radarProductManager = self_->radar_product_manager(); auto radarSite = radarProductManager->radar_site(); - const float gateSize = radarProductManager->gate_size(); const double radarLatitude = radarSite->latitude(); const double radarLongitude = radarSite->longitude(); // Calculate azimuth coordinates timer.start(); + coordinates_.resize(kMaxCoordinates_); + const std::uint16_t numRadials = radialData->number_of_radials(); const std::uint16_t numRangeBins = radialData->number_of_range_bins(); @@ -583,6 +596,10 @@ void Level3RadialView::Impl::ComputeCoordinates( const float range = (static_cast(gate) + gateRangeOffset) * gateSize; const std::size_t offset = static_cast(radialGate) * 2; + if (offset + 1 >= coordinates_.size()) + { + return; + } double latitude = 0.0; double longitude = 0.0; diff --git a/wxdata/include/scwx/common/products.hpp b/wxdata/include/scwx/common/products.hpp index 97d9c324..7451aa39 100644 --- a/wxdata/include/scwx/common/products.hpp +++ b/wxdata/include/scwx/common/products.hpp @@ -73,8 +73,9 @@ Level2Product GetLevel2Product(const std::string& name); const std::string& GetLevel3CategoryName(Level3ProductCategory category); const std::string& GetLevel3CategoryDescription(Level3ProductCategory category); -const std::string& -GetLevel3CategoryDefaultProduct(Level3ProductCategory category); +std::string +GetLevel3CategoryDefaultProduct(Level3ProductCategory category, + const Level3ProductCategoryMap& categoryMap); Level3ProductCategory GetLevel3Category(const std::string& categoryName); Level3ProductCategory GetLevel3CategoryByProduct(const std::string& productName); diff --git a/wxdata/source/scwx/common/products.cpp b/wxdata/source/scwx/common/products.cpp index f3ecd1e8..48969e13 100644 --- a/wxdata/source/scwx/common/products.cpp +++ b/wxdata/source/scwx/common/products.cpp @@ -49,7 +49,7 @@ static const std::unordered_map level3ProductCodeMap_ { {153, "SDR"}, {154, "SDV"}, {159, "DZD"}, {161, "DCC"}, {163, "DKD"}, {165, "DHC"}, {166, "ML"}, {169, "OHA"}, {170, "DAA"}, {172, "DTA"}, {173, "DUA"}, {174, "DOD"}, {175, "DSD"}, {177, "HHC"}, {180, "TDR"}, - {182, "TDV"}}; + {182, "TDV"}, {186, "TZL"}}; static const std::unordered_map level3ProductDescription_ { @@ -84,6 +84,7 @@ static const std::unordered_map {"HHC", "Hybrid Hydrometeor Classification"}, {"TDR", "Digital Reflectivity"}, {"TDV", "Digital Velocity"}, + {"TZL", "Long Range Reflectivity"}, {"?", "Unknown"}}; static const std::unordered_map> @@ -91,6 +92,7 @@ static const std::unordered_map> // Reflectivity {"SDR", {"NXB", "NYB", "NZB", "N0B", "NAB", "N1B", "NBB", "N2B", "N3B"}}, {"DR", {"NXQ", "NYQ", "NZQ", "N0Q", "NAQ", "N1Q", "NBQ", "N2Q", "N3Q"}}, + {"TZL", {"TZL"}}, {"TDR", {"TZ0", "TZ1", "TZ2"}}, {"NCR", {"NCR"}}, @@ -184,7 +186,7 @@ static const std::unordered_map static const std::unordered_map> level3CategoryProductList_ { - {Level3ProductCategory::Reflectivity, {"SDR", "DR", "TDR", "NCR"}}, + {Level3ProductCategory::Reflectivity, {"SDR", "DR", "TZL", "TDR", "NCR"}}, {Level3ProductCategory::Velocity, {"SDV", "DV", "TDV"}}, {Level3ProductCategory::StormRelativeVelocity, {"SRM"}}, {Level3ProductCategory::SpectrumWidth, {"SW"}}, @@ -296,9 +298,27 @@ const std::string& GetLevel3CategoryDescription(Level3ProductCategory category) return level3CategoryDescription_.at(category); } -const std::string& -GetLevel3CategoryDefaultProduct(Level3ProductCategory category) +std::string +GetLevel3CategoryDefaultProduct(Level3ProductCategory category, + const Level3ProductCategoryMap& categoryMap) { + const auto& productsIt = categoryMap.find(category); + if (productsIt == categoryMap.cend()) + { + return level3CategoryDefaultAwipsId_.at(category); + } + + const auto& productsSiteHas = productsIt->second; + const auto& productList = level3CategoryProductList_.at(category); + for (auto& product : productList) + { + const auto& tiltsIt = productsSiteHas.find(product); + if (tiltsIt != productsSiteHas.cend() && tiltsIt->second.size() > 0) + { + return tiltsIt->second[0]; + } + } + return level3CategoryDefaultAwipsId_.at(category); } diff --git a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp index 1a0effaa..4cb525ae 100644 --- a/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/product_description_block.cpp @@ -57,7 +57,7 @@ static const std::unordered_map rangeMap_ { {163, 300}, {165, 300}, {166, 230}, {167, 300}, {168, 300}, {169, 230}, {170, 230}, {171, 230}, {172, 230}, {173, 230}, {174, 230}, {175, 230}, {176, 230}, {177, 230}, {178, 300}, {179, 300}, {180, 89}, {181, 89}, - {182, 89}, {184, 89}, {186, 412}, {193, 460}, {195, 460}, {196, 50}}; + {182, 89}, {184, 89}, {186, 417}, {193, 460}, {195, 460}, {196, 50}}; static const std::unordered_map xResolutionMap_ { {19, 1000}, {20, 2000}, {27, 1000}, {30, 1000}, {31, 2000}, {32, 1000},