diff --git a/.clang-tidy b/.clang-tidy index 1eed15a2..602e3d0c 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -6,6 +6,7 @@ Checks: - 'misc-*' - 'modernize-*' - 'performance-*' + - '-cppcoreguidelines-pro-type-reinterpret-cast' - '-misc-include-cleaner' - '-misc-non-private-member-variables-in-classes' - '-modernize-use-trailing-return-type' diff --git a/scwx-qt/gl/radar.frag b/scwx-qt/gl/radar.frag index 0c605b8a..b6491e8b 100644 --- a/scwx-qt/gl/radar.frag +++ b/scwx-qt/gl/radar.frag @@ -9,14 +9,14 @@ uniform float uDataMomentScale; uniform bool uCFPEnabled; -flat in uint dataMoment; -flat in uint cfpMoment; +in float dataMoment; +in float cfpMoment; layout (location = 0) out vec4 fragColor; void main() { - float texCoord = float(dataMoment - uDataMomentOffset) / uDataMomentScale; + float texCoord = (dataMoment - float(uDataMomentOffset)) / uDataMomentScale; if (uCFPEnabled && cfpMoment > 8u) { diff --git a/scwx-qt/gl/radar.vert b/scwx-qt/gl/radar.vert index b4da9f17..97754b73 100644 --- a/scwx-qt/gl/radar.vert +++ b/scwx-qt/gl/radar.vert @@ -13,8 +13,8 @@ layout (location = 2) in uint aCfpMoment; uniform mat4 uMVPMatrix; uniform vec2 uMapScreenCoord; -flat out uint dataMoment; -flat out uint cfpMoment; +out float dataMoment; +out float cfpMoment; vec2 latLngToScreenCoordinate(in vec2 latLng) { diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 37b8b268..ad504cc0 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -322,6 +322,8 @@ MainWindow::MainWindow(QWidget* parent) : p->mapSettingsGroup_ = new ui::CollapsibleGroup(tr("Map Settings"), this); p->mapSettingsGroup_->GetContentsLayout()->addWidget(ui->mapStyleLabel); p->mapSettingsGroup_->GetContentsLayout()->addWidget(ui->mapStyleComboBox); + p->mapSettingsGroup_->GetContentsLayout()->addWidget( + ui->smoothRadarDataCheckBox); p->mapSettingsGroup_->GetContentsLayout()->addWidget( ui->trackLocationCheckBox); ui->radarToolboxScrollAreaContents->layout()->replaceWidget( @@ -642,6 +644,11 @@ void MainWindow::on_actionDumpRadarProductRecords_triggered() manager::RadarProductManager::DumpRecords(); } +void MainWindow::on_actionRadarWireframe_triggered(bool checked) +{ + p->activeMap_->SetRadarWireframeEnabled(checked); +} + void MainWindow::on_actionUserManual_triggered() { QDesktopServices::openUrl(QUrl {"https://supercell-wx.readthedocs.io/"}); @@ -1085,6 +1092,25 @@ void MainWindowImpl::ConnectOtherSignals() } } }); + connect( + mainWindow_->ui->smoothRadarDataCheckBox, + &QCheckBox::checkStateChanged, + mainWindow_, + [this](Qt::CheckState state) + { + const bool smoothingEnabled = (state == Qt::CheckState::Checked); + + auto it = std::find(maps_.cbegin(), maps_.cend(), activeMap_); + if (it != maps_.cend()) + { + const std::size_t i = std::distance(maps_.cbegin(), it); + settings::MapSettings::Instance().smoothing_enabled(i).StageValue( + smoothingEnabled); + } + + // Turn on smoothing + activeMap_->SetSmoothingEnabled(smoothingEnabled); + }); connect(mainWindow_->ui->trackLocationCheckBox, &QCheckBox::checkStateChanged, mainWindow_, @@ -1471,6 +1497,13 @@ void MainWindowImpl::UpdateRadarProductSettings() { level2SettingsGroup_->setVisible(false); } + + mainWindow_->ui->smoothRadarDataCheckBox->setCheckState( + activeMap_->GetSmoothingEnabled() ? Qt::CheckState::Checked : + Qt::CheckState::Unchecked); + + mainWindow_->ui->actionRadarWireframe->setChecked( + activeMap_->GetRadarWireframeEnabled()); } void MainWindowImpl::UpdateRadarSite() diff --git a/scwx-qt/source/scwx/qt/main/main_window.hpp b/scwx-qt/source/scwx/qt/main/main_window.hpp index 6a4fb5b4..6eb7fee2 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.hpp +++ b/scwx-qt/source/scwx/qt/main/main_window.hpp @@ -29,7 +29,7 @@ class MainWindow : public QMainWindow void keyPressEvent(QKeyEvent* ev) override final; void keyReleaseEvent(QKeyEvent* ev) override final; void showEvent(QShowEvent* event) override; - void closeEvent(QCloseEvent *event) override; + void closeEvent(QCloseEvent* event) override; signals: void ActiveMapMoved(double latitude, double longitude); @@ -49,6 +49,7 @@ private slots: void on_actionImGuiDebug_triggered(); void on_actionDumpLayerList_triggered(); void on_actionDumpRadarProductRecords_triggered(); + void on_actionRadarWireframe_triggered(bool checked); void on_actionUserManual_triggered(); void on_actionDiscord_triggered(); void on_actionGitHubRepository_triggered(); diff --git a/scwx-qt/source/scwx/qt/main/main_window.ui b/scwx-qt/source/scwx/qt/main/main_window.ui index c5e877c9..5d856663 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.ui +++ b/scwx-qt/source/scwx/qt/main/main_window.ui @@ -97,6 +97,8 @@ + + @@ -153,8 +155,8 @@ 0 0 - 205 - 701 + 190 + 680 @@ -329,6 +331,13 @@ + + + + Smooth Radar Data + + + @@ -497,6 +506,14 @@ Location &Marker Manager + + + true + + + Radar &Wireframe + + 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 4659f079..d25bb28d 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -10,7 +10,6 @@ #include #include -#include #include #include #include @@ -28,6 +27,7 @@ #include #include #include +#include #if defined(_MSC_VER) # pragma warning(pop) @@ -63,6 +63,8 @@ static constexpr uint32_t NUM_COORIDNATES_1_DEGREE = static const std::string kDefaultLevel3Product_ {"N0B"}; +static constexpr std::size_t kTimerPlaces_ {6u}; + static constexpr std::chrono::seconds kFastRetryInterval_ {15}; static constexpr std::chrono::seconds kSlowRetryInterval_ {120}; @@ -206,6 +208,13 @@ class RadarProductManagerImpl void UpdateAvailableProductsSync(); + void + CalculateCoordinates(const boost::integer_range& radialGates, + const units::angle::degrees radialAngle, + const units::angle::degrees angleOffset, + const float gateRangeOffset, + std::vector& outputCoordinates); + static void PopulateProductTimes(std::shared_ptr providerManager, RadarProductRecordMap& productRecordMap, @@ -226,10 +235,12 @@ class RadarProductManagerImpl std::size_t cacheLimit_ {6u}; std::vector coordinates0_5Degree_ {}; + std::vector coordinates0_5DegreeSmooth_ {}; std::vector coordinates1Degree_ {}; + std::vector coordinates1DegreeSmooth_ {}; - RadarProductRecordMap level2ProductRecords_ {}; - RadarProductRecordList level2ProductRecentRecords_ {}; + RadarProductRecordMap level2ProductRecords_ {}; + RadarProductRecordList level2ProductRecentRecords_ {}; std::unordered_map level3ProductRecordsMap_ {}; std::unordered_map @@ -361,14 +372,29 @@ void RadarProductManager::DumpRecords() } const std::vector& -RadarProductManager::coordinates(common::RadialSize radialSize) const +RadarProductManager::coordinates(common::RadialSize radialSize, + bool smoothingEnabled) const { switch (radialSize) { case common::RadialSize::_0_5Degree: - return p->coordinates0_5Degree_; + if (smoothingEnabled) + { + return p->coordinates0_5DegreeSmooth_; + } + else + { + return p->coordinates0_5Degree_; + } case common::RadialSize::_1Degree: - return p->coordinates1Degree_; + if (smoothingEnabled) + { + return p->coordinates1DegreeSmooth_; + } + else + { + return p->coordinates1Degree_; + } default: throw std::invalid_argument("Invalid radial size"); } @@ -430,50 +456,51 @@ void RadarProductManager::Initialize() boost::timer::cpu_timer timer; - const GeographicLib::Geodesic& geodesic( - util::GeographicLib::DefaultGeodesic()); - - const QMapLibre::Coordinate radar(p->radarSite_->latitude(), - p->radarSite_->longitude()); - - const float gateSize = gate_size(); - // Calculate half degree azimuth coordinates timer.start(); std::vector& coordinates0_5Degree = p->coordinates0_5Degree_; coordinates0_5Degree.resize(NUM_COORIDNATES_0_5_DEGREE); - auto radialGates0_5Degree = + const auto radialGates0_5Degree = boost::irange(0, NUM_RADIAL_GATES_0_5_DEGREE); - std::for_each( - std::execution::par_unseq, - radialGates0_5Degree.begin(), - radialGates0_5Degree.end(), - [&](uint32_t radialGate) - { - const uint16_t gate = - static_cast(radialGate % common::MAX_DATA_MOMENT_GATES); - const uint16_t radial = - static_cast(radialGate / common::MAX_DATA_MOMENT_GATES); - - const float angle = radial * 0.5f; // 0.5 degree radial - const float range = (gate + 1) * gateSize; - const size_t offset = radialGate * 2; + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers): Values are given + // descriptions + p->CalculateCoordinates( + radialGates0_5Degree, + units::angle::degrees {0.5f}, // Radial angle + units::angle::degrees {0.0f}, // Angle offset + // Far end of the first gate is the gate size distance from the radar site + 1.0f, + coordinates0_5Degree); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) - double latitude; - double longitude; + timer.stop(); + logger_->debug("Coordinates (0.5 degree) calculated in {}", + timer.format(kTimerPlaces_, "%ws")); - geodesic.Direct( - radar.first, radar.second, angle, range, latitude, longitude); + // Calculate half degree smooth azimuth coordinates + timer.start(); + std::vector& coordinates0_5DegreeSmooth = + p->coordinates0_5DegreeSmooth_; + + coordinates0_5DegreeSmooth.resize(NUM_COORIDNATES_0_5_DEGREE); + + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers): Values are given + // descriptions + p->CalculateCoordinates(radialGates0_5Degree, + units::angle::degrees {0.5f}, // Radial angle + units::angle::degrees {0.25f}, // Angle offset + // Center of the first gate is half the gate size + // distance from the radar site + 0.5f, + coordinates0_5DegreeSmooth); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) - coordinates0_5Degree[offset] = latitude; - coordinates0_5Degree[offset + 1] = longitude; - }); timer.stop(); - logger_->debug("Coordinates (0.5 degree) calculated in {}", - timer.format(6, "%ws")); + logger_->debug("Coordinates (0.5 degree smooth) calculated in {}", + timer.format(kTimerPlaces_, "%ws")); // Calculate 1 degree azimuth coordinates timer.start(); @@ -481,38 +508,89 @@ void RadarProductManager::Initialize() coordinates1Degree.resize(NUM_COORIDNATES_1_DEGREE); - auto radialGates1Degree = + const auto radialGates1Degree = boost::irange(0, NUM_RADIAL_GATES_1_DEGREE); + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers): Values are given + // descriptions + p->CalculateCoordinates( + radialGates1Degree, + units::angle::degrees {1.0f}, // Radial angle + units::angle::degrees {0.0f}, // Angle offset + // Far end of the first gate is the gate size distance from the radar site + 1.0f, + coordinates1Degree); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + + timer.stop(); + logger_->debug("Coordinates (1 degree) calculated in {}", + timer.format(kTimerPlaces_, "%ws")); + + // Calculate 1 degree smooth azimuth coordinates + timer.start(); + std::vector& coordinates1DegreeSmooth = p->coordinates1DegreeSmooth_; + + coordinates1DegreeSmooth.resize(NUM_COORIDNATES_1_DEGREE); + + // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers): Values are given + // descriptions + p->CalculateCoordinates(radialGates1Degree, + units::angle::degrees {1.0f}, // Radial angle + units::angle::degrees {0.5f}, // Angle offset + // Center of the first gate is half the gate size + // distance from the radar site + 0.5f, + coordinates1DegreeSmooth); + // NOLINTEND(cppcoreguidelines-avoid-magic-numbers) + + timer.stop(); + logger_->debug("Coordinates (1 degree smooth) calculated in {}", + timer.format(kTimerPlaces_, "%ws")); + + p->initialized_ = true; +} + +void RadarProductManagerImpl::CalculateCoordinates( + const boost::integer_range& radialGates, + const units::angle::degrees radialAngle, + const units::angle::degrees angleOffset, + const float gateRangeOffset, + std::vector& outputCoordinates) +{ + const GeographicLib::Geodesic& geodesic( + util::GeographicLib::DefaultGeodesic()); + + const QMapLibre::Coordinate radar(radarSite_->latitude(), + radarSite_->longitude()); + + const float gateSize = self_->gate_size(); + std::for_each( std::execution::par_unseq, - radialGates1Degree.begin(), - radialGates1Degree.end(), + radialGates.begin(), + radialGates.end(), [&](uint32_t radialGate) { - const uint16_t gate = - static_cast(radialGate % common::MAX_DATA_MOMENT_GATES); - const uint16_t radial = - static_cast(radialGate / common::MAX_DATA_MOMENT_GATES); + const auto gate = static_cast( + radialGate % common::MAX_DATA_MOMENT_GATES); + const auto radial = static_cast( + radialGate / common::MAX_DATA_MOMENT_GATES); - const float angle = radial * 1.0f; // 1 degree radial - const float range = (gate + 1) * gateSize; - const size_t offset = radialGate * 2; + const float angle = static_cast(radial) * radialAngle.value() + + angleOffset.value(); + const float range = + (static_cast(gate) + gateRangeOffset) * gateSize; + const std::size_t offset = static_cast(radialGate) * 2; - double latitude; - double longitude; + double latitude = 0.0; + double longitude = 0.0; geodesic.Direct( radar.first, radar.second, angle, range, latitude, longitude); - coordinates1Degree[offset] = latitude; - coordinates1Degree[offset + 1] = longitude; + outputCoordinates[offset] = static_cast(latitude); + outputCoordinates[offset + 1] = static_cast(longitude); }); - timer.stop(); - logger_->debug("Coordinates (1 degree) calculated in {}", - timer.format(6, "%ws")); - - p->initialized_ = true; } std::shared_ptr 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 17dfa551..3f4899ea 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.hpp @@ -41,11 +41,12 @@ class RadarProductManager : public QObject */ static void DumpRecords(); - const std::vector& coordinates(common::RadialSize radialSize) const; - const scwx::util::time_zone* default_time_zone() const; - float gate_size() const; - std::string radar_id() const; - std::shared_ptr radar_site() const; + [[nodiscard]] const std::vector& + coordinates(common::RadialSize radialSize, bool smoothingEnabled) const; + [[nodiscard]] const scwx::util::time_zone* default_time_zone() const; + [[nodiscard]] float gate_size() const; + [[nodiscard]] std::string radar_id() const; + [[nodiscard]] std::shared_ptr radar_site() const; void Initialize(); diff --git a/scwx-qt/source/scwx/qt/map/map_settings.hpp b/scwx-qt/source/scwx/qt/map/map_settings.hpp index 642c8fa1..a015aca3 100644 --- a/scwx-qt/source/scwx/qt/map/map_settings.hpp +++ b/scwx-qt/source/scwx/qt/map/map_settings.hpp @@ -9,16 +9,17 @@ namespace map struct MapSettings { - explicit MapSettings() : isActive_ {false} {} - ~MapSettings() = default; + explicit MapSettings() = default; + ~MapSettings() = default; - MapSettings(const MapSettings&) = delete; + MapSettings(const MapSettings&) = delete; MapSettings& operator=(const MapSettings&) = delete; - MapSettings(MapSettings&&) noexcept = default; + MapSettings(MapSettings&&) noexcept = default; MapSettings& operator=(MapSettings&&) noexcept = default; - bool isActive_; + bool isActive_ {false}; + bool radarWireframeEnabled_ {false}; }; } // namespace map diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 9c222ef6..df45024a 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -105,13 +106,16 @@ class MapWidgetImpl : public QObject map::AlertLayer::InitializeHandler(); auto& generalSettings = settings::GeneralSettings::Instance(); + auto& mapSettings = settings::MapSettings::Instance(); // Initialize context context_->set_map_provider( GetMapProvider(generalSettings.map_provider().GetValue())); context_->set_overlay_product_view(overlayProductView); + // Initialize map data SetRadarSite(generalSettings.default_radar_site().GetValue()); + smoothingEnabled_ = mapSettings.smoothing_enabled(id).GetValue(); // Create ImGui Context static size_t currentMapId_ {0u}; @@ -225,7 +229,7 @@ class MapWidgetImpl : public QObject std::shared_ptr overlayLayer_; std::shared_ptr overlayProductLayer_ {nullptr}; std::shared_ptr placefileLayer_; - std::shared_ptr markerLayer_; + std::shared_ptr markerLayer_; std::shared_ptr colorTableLayer_; std::shared_ptr radarSiteLayer_ {nullptr}; @@ -233,6 +237,7 @@ class MapWidgetImpl : public QObject bool autoRefreshEnabled_; bool autoUpdateEnabled_; + bool smoothingEnabled_ {false}; common::Level2Product selectedLevel2Product_; @@ -727,6 +732,35 @@ std::uint16_t MapWidget::GetVcp() const } } +bool MapWidget::GetRadarWireframeEnabled() const +{ + return p->context_->settings().radarWireframeEnabled_; +} + +void MapWidget::SetRadarWireframeEnabled(bool wireframeEnabled) +{ + p->context_->settings().radarWireframeEnabled_ = wireframeEnabled; + QMetaObject::invokeMethod( + this, static_cast(&QWidget::update)); +} + +bool MapWidget::GetSmoothingEnabled() const +{ + return p->smoothingEnabled_; +} + +void MapWidget::SetSmoothingEnabled(bool smoothingEnabled) +{ + p->smoothingEnabled_ = smoothingEnabled; + + auto radarProductView = p->context_->radar_product_view(); + if (radarProductView != nullptr) + { + radarProductView->set_smoothing_enabled(smoothingEnabled); + radarProductView->Update(); + } +} + void MapWidget::SelectElevation(float elevation) { auto radarProductView = p->context_->radar_product_view(); @@ -775,6 +809,7 @@ void MapWidget::SelectRadarProduct(common::RadarProductGroup group, radarProductView = view::RadarProductViewFactory::Create( group, productName, productCode, p->radarProductManager_); + radarProductView->set_smoothing_enabled(p->smoothingEnabled_); p->context_->set_radar_product_view(radarProductView); p->RadarProductViewConnect(); diff --git a/scwx-qt/source/scwx/qt/map/map_widget.hpp b/scwx-qt/source/scwx/qt/map/map_widget.hpp index 40f7df77..23d38680 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.hpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.hpp @@ -39,16 +39,19 @@ class MapWidget : public QOpenGLWidget void DumpLayerList() const; - common::Level3ProductCategoryMap GetAvailableLevel3Categories(); - float GetElevation() const; - std::vector GetElevationCuts() const; - std::vector GetLevel3Products(); - std::string GetMapStyle() const; - common::RadarProductGroup GetRadarProductGroup() const; - std::string GetRadarProductName() const; - std::shared_ptr GetRadarSite() const; - std::chrono::system_clock::time_point GetSelectedTime() const; - std::uint16_t GetVcp() const; + [[nodiscard]] common::Level3ProductCategoryMap + GetAvailableLevel3Categories(); + [[nodiscard]] float GetElevation() const; + [[nodiscard]] std::vector GetElevationCuts() const; + [[nodiscard]] std::vector GetLevel3Products(); + [[nodiscard]] std::string GetMapStyle() const; + [[nodiscard]] common::RadarProductGroup GetRadarProductGroup() const; + [[nodiscard]] std::string GetRadarProductName() const; + [[nodiscard]] std::shared_ptr GetRadarSite() const; + [[nodiscard]] bool GetRadarWireframeEnabled() const; + [[nodiscard]] std::chrono::system_clock::time_point GetSelectedTime() const; + [[nodiscard]] bool GetSmoothingEnabled() const; + [[nodiscard]] std::uint16_t GetVcp() const; void SelectElevation(float elevation); @@ -117,6 +120,8 @@ class MapWidget : public QOpenGLWidget double pitch); void SetInitialMapStyle(const std::string& styleName); void SetMapStyle(const std::string& styleName); + void SetRadarWireframeEnabled(bool enabled); + void SetSmoothingEnabled(bool enabled); /** * Updates the coordinates associated with mouse movement from another map. 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 be564926..8d243973 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -1,12 +1,11 @@ #include +#include #include #include #include #include #include -#include - #if defined(_MSC_VER) # pragma warning(push, 0) #endif @@ -267,6 +266,13 @@ void RadarProductLayer::Render( // Set OpenGL blend mode for transparency gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + const bool wireframeEnabled = context()->settings().radarWireframeEnabled_; + if (wireframeEnabled) + { + // Set polygon mode to draw wireframe + gl.glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + } + if (p->colorTableNeedsUpdate_) { UpdateColorTable(); @@ -303,6 +309,12 @@ void RadarProductLayer::Render( gl.glBindVertexArray(p->vao_); gl.glDrawArrays(GL_TRIANGLES, 0, p->numVertices_); + if (wireframeEnabled) + { + // Restore polygon mode to default + gl.glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + } + SCWX_GL_CHECK_ERROR(); } diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.cpp b/scwx-qt/source/scwx/qt/settings/map_settings.cpp index f0efea78..e1c8276e 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.cpp @@ -6,7 +6,6 @@ #include #include -#include #include @@ -27,12 +26,15 @@ static const std::string kMapStyleName_ {"map_style"}; static const std::string kRadarSiteName_ {"radar_site"}; static const std::string kRadarProductGroupName_ {"radar_product_group"}; static const std::string kRadarProductName_ {"radar_product"}; +static const std::string kSmoothingEnabledName_ {"smoothing_enabled"}; -static const std::string kDefaultMapStyle_ {"?"}; +static const std::string kDefaultMapStyle_ {"?"}; static const std::string kDefaultRadarProductGroupString_ = "L3"; static const std::array kDefaultRadarProduct_ { "N0B", "N0G", "N0C", "N0X"}; +static constexpr bool kDefaultSmoothingEnabled_ {false}; + class MapSettings::Impl { public: @@ -43,26 +45,28 @@ class MapSettings::Impl SettingsVariable radarProductGroup_ { kRadarProductGroupName_}; SettingsVariable radarProduct_ {kRadarProductName_}; + SettingsVariable smoothingEnabled_ {kSmoothingEnabledName_}; }; explicit Impl() { for (std::size_t i = 0; i < kCount_; i++) { - map_[i].mapStyle_.SetDefault(kDefaultMapStyle_); - map_[i].radarSite_.SetDefault(kDefaultRadarSite_); - map_[i].radarProductGroup_.SetDefault( + map_.at(i).mapStyle_.SetDefault(kDefaultMapStyle_); + map_.at(i).radarSite_.SetDefault(kDefaultRadarSite_); + map_.at(i).radarProductGroup_.SetDefault( kDefaultRadarProductGroupString_); - map_[i].radarProduct_.SetDefault(kDefaultRadarProduct_[i]); + map_.at(i).radarProduct_.SetDefault(kDefaultRadarProduct_.at(i)); + map_.at(i).smoothingEnabled_.SetDefault(kDefaultSmoothingEnabled_); - map_[i].radarSite_.SetValidator( + map_.at(i).radarSite_.SetValidator( [](const std::string& value) { // Radar site must exist return config::RadarSite::Get(value) != nullptr; }); - map_[i].radarProductGroup_.SetValidator( + map_.at(i).radarProductGroup_.SetValidator( [](const std::string& value) { // Radar product group must be valid @@ -71,12 +75,12 @@ class MapSettings::Impl return radarProductGroup != common::RadarProductGroup::Unknown; }); - map_[i].radarProduct_.SetValidator( + map_.at(i).radarProduct_.SetValidator( [this, i](const std::string& value) { common::RadarProductGroup radarProductGroup = common::GetRadarProductGroup( - map_[i].radarProductGroup_.GetValue()); + map_.at(i).radarProductGroup_.GetValue()); if (radarProductGroup == common::RadarProductGroup::Level2) { @@ -92,10 +96,11 @@ class MapSettings::Impl }); variables_.insert(variables_.cend(), - {&map_[i].mapStyle_, - &map_[i].radarSite_, - &map_[i].radarProductGroup_, - &map_[i].radarProduct_}); + {&map_.at(i).mapStyle_, + &map_.at(i).radarSite_, + &map_.at(i).radarProductGroup_, + &map_.at(i).radarProduct_, + &map_.at(i).smoothingEnabled_}); } } @@ -103,10 +108,11 @@ class MapSettings::Impl void SetDefaults(std::size_t i) { - map_[i].mapStyle_.SetValueToDefault(); - map_[i].radarSite_.SetValueToDefault(); - map_[i].radarProductGroup_.SetValueToDefault(); - map_[i].radarProduct_.SetValueToDefault(); + map_.at(i).mapStyle_.SetValueToDefault(); + map_.at(i).radarSite_.SetValueToDefault(); + map_.at(i).radarProductGroup_.SetValueToDefault(); + map_.at(i).radarProduct_.SetValueToDefault(); + map_.at(i).smoothingEnabled_.SetValueToDefault(); } friend void tag_invoke(boost::json::value_from_tag, @@ -116,7 +122,8 @@ class MapSettings::Impl jv = {{kMapStyleName_, data.mapStyle_.GetValue()}, {kRadarSiteName_, data.radarSite_.GetValue()}, {kRadarProductGroupName_, data.radarProductGroup_.GetValue()}, - {kRadarProductName_, data.radarProduct_.GetValue()}}; + {kRadarProductName_, data.radarProduct_.GetValue()}, + {kSmoothingEnabledName_, data.smoothingEnabled_.GetValue()}}; } friend bool operator==(const MapData& lhs, const MapData& rhs) @@ -124,7 +131,8 @@ class MapSettings::Impl return (lhs.mapStyle_ == rhs.mapStyle_ && // lhs.radarSite_ == rhs.radarSite_ && lhs.radarProductGroup_ == rhs.radarProductGroup_ && - lhs.radarProduct_ == rhs.radarProduct_); + lhs.radarProduct_ == rhs.radarProduct_ && + lhs.smoothingEnabled_ == rhs.smoothingEnabled_); } std::array map_ {}; @@ -149,25 +157,29 @@ std::size_t MapSettings::count() const return kCount_; } -SettingsVariable& MapSettings::map_style(std::size_t i) const +SettingsVariable& MapSettings::map_style(std::size_t i) { - return p->map_[i].mapStyle_; + return p->map_.at(i).mapStyle_; } -SettingsVariable& MapSettings::radar_site(std::size_t i) const +SettingsVariable& MapSettings::radar_site(std::size_t i) { - return p->map_[i].radarSite_; + return p->map_.at(i).radarSite_; } -SettingsVariable& -MapSettings::radar_product_group(std::size_t i) const +SettingsVariable& MapSettings::radar_product_group(std::size_t i) { - return p->map_[i].radarProductGroup_; + return p->map_.at(i).radarProductGroup_; } -SettingsVariable& MapSettings::radar_product(std::size_t i) const +SettingsVariable& MapSettings::radar_product(std::size_t i) { - return p->map_[i].radarProduct_; + return p->map_.at(i).radarProduct_; +} + +SettingsVariable& MapSettings::smoothing_enabled(std::size_t i) +{ + return p->map_.at(i).smoothingEnabled_; } bool MapSettings::Shutdown() @@ -177,9 +189,10 @@ bool MapSettings::Shutdown() // Commit settings that are managed separate from the settings dialog for (std::size_t i = 0; i < kCount_; ++i) { - Impl::MapData& mapRecordSettings = p->map_[i]; + Impl::MapData& mapRecordSettings = p->map_.at(i); dataChanged |= mapRecordSettings.mapStyle_.Commit(); + dataChanged |= mapRecordSettings.smoothingEnabled_.Commit(); } return dataChanged; @@ -200,13 +213,15 @@ bool MapSettings::ReadJson(const boost::json::object& json) if (i < mapArray.size() && mapArray.at(i).is_object()) { const boost::json::object& mapRecord = mapArray.at(i).as_object(); - Impl::MapData& mapRecordSettings = p->map_[i]; + Impl::MapData& mapRecordSettings = p->map_.at(i); // Load JSON Elements validated &= mapRecordSettings.mapStyle_.ReadValue(mapRecord); validated &= mapRecordSettings.radarSite_.ReadValue(mapRecord); validated &= mapRecordSettings.radarProductGroup_.ReadValue(mapRecord); + validated &= + mapRecordSettings.smoothingEnabled_.ReadValue(mapRecord); bool productValidated = mapRecordSettings.radarProduct_.ReadValue(mapRecord); diff --git a/scwx-qt/source/scwx/qt/settings/map_settings.hpp b/scwx-qt/source/scwx/qt/settings/map_settings.hpp index c8726491..36ce6464 100644 --- a/scwx-qt/source/scwx/qt/settings/map_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/map_settings.hpp @@ -26,10 +26,11 @@ class MapSettings : public SettingsCategory MapSettings& operator=(MapSettings&&) noexcept; std::size_t count() const; - SettingsVariable& map_style(std::size_t i) const; - SettingsVariable& radar_site(std::size_t i) const; - SettingsVariable& radar_product_group(std::size_t i) const; - SettingsVariable& radar_product(std::size_t i) const; + SettingsVariable& map_style(std::size_t i); + SettingsVariable& radar_site(std::size_t i); + SettingsVariable& radar_product_group(std::size_t i); + SettingsVariable& radar_product(std::size_t i); + SettingsVariable& smoothing_enabled(std::size_t i); bool Shutdown(); diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.cpp b/scwx-qt/source/scwx/qt/settings/product_settings.cpp index 3cf47ef7..b9287474 100644 --- a/scwx-qt/source/scwx/qt/settings/product_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/product_settings.cpp @@ -15,12 +15,15 @@ class ProductSettings::Impl public: explicit Impl() { + showSmoothedRangeFolding_.SetDefault(false); stiForecastEnabled_.SetDefault(true); stiPastEnabled_.SetDefault(true); } ~Impl() {} + SettingsVariable showSmoothedRangeFolding_ { + "show_smoothed_range_folding"}; SettingsVariable stiForecastEnabled_ {"sti_forecast_enabled"}; SettingsVariable stiPastEnabled_ {"sti_past_enabled"}; }; @@ -28,7 +31,9 @@ class ProductSettings::Impl ProductSettings::ProductSettings() : SettingsCategory("product"), p(std::make_unique()) { - RegisterVariables({&p->stiForecastEnabled_, &p->stiPastEnabled_}); + RegisterVariables({&p->showSmoothedRangeFolding_, + &p->stiForecastEnabled_, + &p->stiPastEnabled_}); SetDefaults(); } ProductSettings::~ProductSettings() = default; @@ -37,12 +42,17 @@ ProductSettings::ProductSettings(ProductSettings&&) noexcept = default; ProductSettings& ProductSettings::operator=(ProductSettings&&) noexcept = default; -SettingsVariable& ProductSettings::sti_forecast_enabled() const +SettingsVariable& ProductSettings::show_smoothed_range_folding() +{ + return p->showSmoothedRangeFolding_; +} + +SettingsVariable& ProductSettings::sti_forecast_enabled() { return p->stiForecastEnabled_; } -SettingsVariable& ProductSettings::sti_past_enabled() const +SettingsVariable& ProductSettings::sti_past_enabled() { return p->stiPastEnabled_; } @@ -66,7 +76,9 @@ ProductSettings& ProductSettings::Instance() bool operator==(const ProductSettings& lhs, const ProductSettings& rhs) { - return (lhs.p->stiForecastEnabled_ == rhs.p->stiForecastEnabled_ && + return (lhs.p->showSmoothedRangeFolding_ == + rhs.p->showSmoothedRangeFolding_ && + lhs.p->stiForecastEnabled_ == rhs.p->stiForecastEnabled_ && lhs.p->stiPastEnabled_ == rhs.p->stiPastEnabled_); } diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.hpp b/scwx-qt/source/scwx/qt/settings/product_settings.hpp index c7c09dd8..570c6b15 100644 --- a/scwx-qt/source/scwx/qt/settings/product_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/product_settings.hpp @@ -25,8 +25,9 @@ class ProductSettings : public SettingsCategory ProductSettings(ProductSettings&&) noexcept; ProductSettings& operator=(ProductSettings&&) noexcept; - SettingsVariable& sti_forecast_enabled() const; - SettingsVariable& sti_past_enabled() const; + SettingsVariable& show_smoothed_range_folding(); + SettingsVariable& sti_forecast_enabled(); + SettingsVariable& sti_past_enabled(); static ProductSettings& Instance(); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 69c3a387..1872be26 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -136,6 +137,7 @@ class SettingsDialogImpl &showMapAttribution_, &showMapCenter_, &showMapLogo_, + &showSmoothedRangeFolding_, &updateNotificationsEnabled_, &cursorIconAlwaysOn_, &debugEnabled_, @@ -251,6 +253,7 @@ class SettingsDialogImpl settings::SettingsInterface showMapAttribution_ {}; settings::SettingsInterface showMapCenter_ {}; settings::SettingsInterface showMapLogo_ {}; + settings::SettingsInterface showSmoothedRangeFolding_ {}; settings::SettingsInterface updateNotificationsEnabled_ {}; settings::SettingsInterface cursorIconAlwaysOn_ {}; settings::SettingsInterface debugEnabled_ {}; @@ -527,21 +530,22 @@ void SettingsDialogImpl::SetupGeneralTab() { settings::GeneralSettings& generalSettings = settings::GeneralSettings::Instance(); - + settings::ProductSettings& productSettings = + settings::ProductSettings::Instance(); QObject::connect( - self_->ui->themeComboBox, - &QComboBox::currentTextChanged, - self_, - [this](const QString& text) - { - types::UiStyle style = types::GetUiStyle(text.toStdString()); - bool themeFileEnabled = style == types::UiStyle::FusionCustom; + self_->ui->themeComboBox, + &QComboBox::currentTextChanged, + self_, + [this](const QString& text) + { + const types::UiStyle style = types::GetUiStyle(text.toStdString()); + const bool themeFileEnabled = style == types::UiStyle::FusionCustom; - self_->ui->themeFileLineEdit->setEnabled(themeFileEnabled); - self_->ui->themeFileSelectButton->setEnabled(themeFileEnabled); - self_->ui->resetThemeFileButton->setEnabled(themeFileEnabled); - }); + self_->ui->themeFileLineEdit->setEnabled(themeFileEnabled); + self_->ui->themeFileSelectButton->setEnabled(themeFileEnabled); + self_->ui->resetThemeFileButton->setEnabled(themeFileEnabled); + }); theme_.SetSettingsVariable(generalSettings.theme()); SCWX_SETTINGS_COMBO_BOX(theme_, @@ -759,6 +763,11 @@ void SettingsDialogImpl::SetupGeneralTab() showMapLogo_.SetSettingsVariable(generalSettings.show_map_logo()); showMapLogo_.SetEditWidget(self_->ui->showMapLogoCheckBox); + showSmoothedRangeFolding_.SetSettingsVariable( + productSettings.show_smoothed_range_folding()); + showSmoothedRangeFolding_.SetEditWidget( + self_->ui->showSmoothedRangeFoldingCheckBox); + updateNotificationsEnabled_.SetSettingsVariable( generalSettings.update_notifications_enabled()); updateNotificationsEnabled_.SetEditWidget( diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index 12bcbc0e..ec400682 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -135,9 +135,9 @@ 0 - -246 - 511 - 703 + -272 + 513 + 702 @@ -562,6 +562,19 @@ + + + + false + + + + + + Multi-Pane Cursor Marker Always On + + + @@ -584,22 +597,16 @@ - + - Update Notifications Enabled + Show Range Folding when Smoothing Radar Data - - - false - - - - + - Multi-Pane Cursor Marker Always On + Update Notifications Enabled 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 a5a26157..8dc44ab2 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.cpp @@ -25,6 +25,11 @@ static constexpr std::uint32_t kMaxRadialGates_ = common::MAX_0_5_DEGREE_RADIALS * common::MAX_DATA_MOMENT_GATES; static constexpr std::uint32_t kMaxCoordinates_ = kMaxRadialGates_ * 2u; +static constexpr std::uint8_t kDataWordSize8_ = 8u; + +static constexpr std::size_t kVerticesPerGate_ = 6u; +static constexpr std::size_t kVerticesPerOriginGate_ = 3u; + static constexpr uint16_t RANGE_FOLDED = 1u; static constexpr uint32_t VERTICES_PER_BIN = 6u; static constexpr uint32_t VALUES_PER_VERTEX = 2u; @@ -53,11 +58,10 @@ static const std::unordered_map {common::Level2Product::CorrelationCoefficient, "%"}, {common::Level2Product::ClutterFilterPowerRemoved, "dB"}}; -class Level2ProductViewImpl +class Level2ProductView::Impl { public: - explicit Level2ProductViewImpl(Level2ProductView* self, - common::Level2Product product) : + explicit Impl(Level2ProductView* self, common::Level2Product product) : self_ {self}, product_ {product}, selectedElevation_ {0.0f}, @@ -94,7 +98,7 @@ class Level2ProductViewImpl UpdateOtherUnits(unitSettings.other_units().GetValue()); UpdateSpeedUnits(unitSettings.speed_units().GetValue()); } - ~Level2ProductViewImpl() + ~Impl() { auto& unitSettings = settings::UnitSettings::Instance(); @@ -106,23 +110,36 @@ class Level2ProductViewImpl threadPool_.join(); }; + Impl(const Impl&) = delete; + Impl& operator=(const Impl&) = delete; + + Impl(Impl&&) noexcept = delete; + Impl& operator=(Impl&&) noexcept = delete; + void ComputeCoordinates( - const std::shared_ptr& radarData); + const std::shared_ptr& radarData, + bool smoothingEnabled); void SetProduct(const std::string& productName); void SetProduct(common::Level2Product product); void UpdateOtherUnits(const std::string& name); void UpdateSpeedUnits(const std::string& name); + void ComputeEdgeValue(); + template + [[nodiscard]] inline T RemapDataMoment(T dataMoment) const; + static bool IsRadarDataIncomplete( const std::shared_ptr& radarData); + static units::degrees NormalizeAngle(units::degrees angle); Level2ProductView* self_; boost::asio::thread_pool threadPool_ {1u}; common::Level2Product product_; - wsr88d::rda::DataBlockType dataBlockType_; + wsr88d::rda::DataBlockType dataBlockType_ { + wsr88d::rda::DataBlockType::Unknown}; float selectedElevation_; @@ -130,11 +147,17 @@ class Level2ProductViewImpl std::shared_ptr momentDataBlock0_; + bool lastShowSmoothedRangeFolding_ {false}; + bool lastSmoothingEnabled_ {false}; + std::vector coordinates_ {}; std::vector vertices_ {}; std::vector dataMoments8_ {}; std::vector dataMoments16_ {}; std::vector cfpMoments_ {}; + std::uint16_t edgeValue_ {}; + + bool showSmoothedRangeFolding_ {false}; float latitude_; float longitude_; @@ -164,7 +187,7 @@ Level2ProductView::Level2ProductView( common::Level2Product product, std::shared_ptr radarProductManager) : RadarProductView(radarProductManager), - p(std::make_unique(this, product)) + p(std::make_unique(this, product)) { ConnectRadarProductManager(); } @@ -379,12 +402,12 @@ void Level2ProductView::SelectProduct(const std::string& productName) p->SetProduct(productName); } -void Level2ProductViewImpl::SetProduct(const std::string& productName) +void Level2ProductView::Impl::SetProduct(const std::string& productName) { SetProduct(common::GetLevel2Product(productName)); } -void Level2ProductViewImpl::SetProduct(common::Level2Product product) +void Level2ProductView::Impl::SetProduct(common::Level2Product product) { product_ = product; @@ -401,12 +424,12 @@ void Level2ProductViewImpl::SetProduct(common::Level2Product product) } } -void Level2ProductViewImpl::UpdateOtherUnits(const std::string& name) +void Level2ProductView::Impl::UpdateOtherUnits(const std::string& name) { otherUnits_ = types::GetOtherUnitsFromName(name); } -void Level2ProductViewImpl::UpdateSpeedUnits(const std::string& name) +void Level2ProductView::Impl::UpdateSpeedUnits(const std::string& name) { speedUnits_ = types::GetSpeedUnitsFromName(name); } @@ -511,6 +534,9 @@ void Level2ProductView::ComputeSweep() std::shared_ptr radarProductManager = radar_product_manager(); + const bool smoothingEnabled = smoothing_enabled(); + p->showSmoothedRangeFolding_ = show_smoothed_range_folding(); + const bool& showSmoothedRangeFolding = p->showSmoothedRangeFolding_; std::shared_ptr radarData; std::chrono::system_clock::time_point requestedTime {selected_time()}; @@ -523,12 +549,18 @@ void Level2ProductView::ComputeSweep() Q_EMIT SweepNotComputed(types::NoUpdateReason::NotLoaded); return; } - if (radarData == p->elevationScan_) + if (radarData == p->elevationScan_ && + smoothingEnabled == p->lastSmoothingEnabled_ && + (showSmoothedRangeFolding == p->lastShowSmoothedRangeFolding_ || + !smoothingEnabled)) { Q_EMIT SweepNotComputed(types::NoUpdateReason::NoChange); return; } + p->lastShowSmoothedRangeFolding_ = showSmoothedRangeFolding; + p->lastSmoothingEnabled_ = smoothingEnabled; + logger_->debug("Computing Sweep"); std::size_t radials = radarData->crbegin()->first + 1; @@ -536,8 +568,7 @@ void Level2ProductView::ComputeSweep() // When there is missing data, insert another empty vertex radial at the end // to avoid stretching - const bool isRadarDataIncomplete = - Level2ProductViewImpl::IsRadarDataIncomplete(radarData); + const bool isRadarDataIncomplete = Impl::IsRadarDataIncomplete(radarData); if (isRadarDataIncomplete) { ++vertexRadials; @@ -548,7 +579,7 @@ void Level2ProductView::ComputeSweep() vertexRadials = std::min(vertexRadials, common::MAX_0_5_DEGREE_RADIALS); - p->ComputeCoordinates(radarData); + p->ComputeCoordinates(radarData, smoothingEnabled); const std::vector& coordinates = p->coordinates_; @@ -627,11 +658,20 @@ void Level2ProductView::ComputeSweep() // Start radial is always 0, as coordinates are calculated for each sweep constexpr std::uint16_t startRadial = 0u; - for (auto& radialPair : *radarData) + // For most products other than reflectivity, the edge should not go to the + // bottom of the color table + if (smoothingEnabled) { + p->ComputeEdgeValue(); + } + + for (auto it = radarData->cbegin(); it != radarData->cend(); ++it) + { + const auto& radialPair = *it; std::uint16_t radial = radialPair.first; - auto& radialData = radialPair.second; - auto momentData = radialData->moment_data_block(p->dataBlockType_); + const auto& radialData = radialPair.second; + const std::shared_ptr + momentData = radialData->moment_data_block(p->dataBlockType_); if (momentData0->data_word_size() != momentData->data_word_size()) { @@ -653,7 +693,7 @@ void Level2ProductView::ComputeSweep() std::max(1, dataMomentInterval / gateSizeMeters); // Compute gate range [startGate, endGate) - const std::int32_t startGate = + std::int32_t startGate = (dataMomentRange - dataMomentIntervalH) / gateSizeMeters; const std::int32_t numberOfDataMomentGates = std::min(momentData->number_of_data_moment_gates(), @@ -662,9 +702,19 @@ void Level2ProductView::ComputeSweep() startGate + numberOfDataMomentGates * gateSize, static_cast(common::MAX_DATA_MOMENT_GATES)); - const std::uint8_t* dataMomentsArray8 = nullptr; - const std::uint16_t* dataMomentsArray16 = nullptr; - const std::uint8_t* cfpMomentsArray = nullptr; + if (smoothingEnabled) + { + // If smoothing is enabled, the start gate is incremented by one, as we + // are skipping the radar site origin. The end gate is unaffected, as + // we need to draw one less data point. + ++startGate; + } + + const std::uint8_t* dataMomentsArray8 = nullptr; + const std::uint16_t* dataMomentsArray16 = nullptr; + const std::uint8_t* nextDataMomentsArray8 = nullptr; + const std::uint16_t* nextDataMomentsArray16 = nullptr; + const std::uint8_t* cfpMomentsArray = nullptr; if (momentData->data_word_size() == 8) { @@ -684,6 +734,45 @@ void Level2ProductView::ComputeSweep() ->data_moments()); } + std::shared_ptr + nextMomentData = nullptr; + std::int32_t numberOfNextDataMomentGates = 0; + if (smoothingEnabled) + { + // Smoothing requires the next radial pair as well + auto nextIt = std::next(it); + if (nextIt == radarData->cend()) + { + nextIt = radarData->cbegin(); + } + + const auto& nextRadialPair = *(nextIt); + const auto& nextRadialData = nextRadialPair.second; + nextMomentData = nextRadialData->moment_data_block(p->dataBlockType_); + + if (momentData->data_word_size() != nextMomentData->data_word_size()) + { + // Data should be consistent between radials + logger_->warn("Invalid data moment size"); + continue; + } + + if (nextMomentData->data_word_size() == kDataWordSize8_) + { + nextDataMomentsArray8 = reinterpret_cast( + nextMomentData->data_moments()); + } + else + { + nextDataMomentsArray16 = reinterpret_cast( + nextMomentData->data_moments()); + } + + numberOfNextDataMomentGates = std::min( + nextMomentData->number_of_data_moment_gates(), + static_cast(gates)); + } + for (std::int32_t gate = startGate, i = 0; gate + gateSize <= endGate; gate += gateSize, ++i) { @@ -692,57 +781,172 @@ void Level2ProductView::ComputeSweep() continue; } - std::size_t vertexCount = (gate > 0) ? 6 : 3; + const std::size_t vertexCount = + (gate > 0) ? kVerticesPerGate_ : kVerticesPerOriginGate_; + + // Allow pointer arithmetic here, as bounds have already been checked + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) // Store data moment value if (dataMomentsArray8 != nullptr) { - std::uint8_t dataValue = dataMomentsArray8[i]; - if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + if (!smoothingEnabled) { - continue; - } + const std::uint8_t& dataValue = dataMomentsArray8[i]; + if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + { + continue; + } - for (std::size_t m = 0; m < vertexCount; m++) + for (std::size_t m = 0; m < vertexCount; m++) + { + dataMoments8[mIndex++] = dataValue; + + if (cfpMomentsArray != nullptr) + { + cfpMoments[mIndex - 1] = cfpMomentsArray[i]; + } + } + } + else if (gate > 0) { - dataMoments8[mIndex++] = dataMomentsArray8[i]; + // Validate indices are all in range + if (i + 1 >= numberOfDataMomentGates || + i + 1 >= numberOfNextDataMomentGates) + { + continue; + } - if (cfpMomentsArray != nullptr) + const std::uint8_t& dm1 = dataMomentsArray8[i]; + const std::uint8_t& dm2 = dataMomentsArray8[i + 1]; + const std::uint8_t& dm3 = nextDataMomentsArray8[i]; + const std::uint8_t& dm4 = nextDataMomentsArray8[i + 1]; + + if ((!showSmoothedRangeFolding && // + (dm1 < snrThreshold || dm1 == RANGE_FOLDED) && + (dm2 < snrThreshold || dm2 == RANGE_FOLDED) && + (dm3 < snrThreshold || dm3 == RANGE_FOLDED) && + (dm4 < snrThreshold || dm4 == RANGE_FOLDED)) || + (showSmoothedRangeFolding && // + dm1 < snrThreshold && dm1 != RANGE_FOLDED && + dm2 < snrThreshold && dm2 != RANGE_FOLDED && + dm3 < snrThreshold && dm3 != RANGE_FOLDED && + dm4 < snrThreshold && dm4 != RANGE_FOLDED)) { - cfpMoments[mIndex - 1] = cfpMomentsArray[i]; + // Skip only if all data moments are hidden + continue; } + + // The order must match the store vertices section below + dataMoments8[mIndex++] = p->RemapDataMoment(dm1); + dataMoments8[mIndex++] = p->RemapDataMoment(dm2); + dataMoments8[mIndex++] = p->RemapDataMoment(dm4); + dataMoments8[mIndex++] = p->RemapDataMoment(dm1); + dataMoments8[mIndex++] = p->RemapDataMoment(dm3); + dataMoments8[mIndex++] = p->RemapDataMoment(dm4); + + // cfpMoments is unused, so not populated here + } + else + { + // If smoothing is enabled, gate should never start at zero + // (radar site origin) + logger_->error( + "Smoothing enabled, gate should not start at zero"); + continue; } } else { - std::uint16_t dataValue = dataMomentsArray16[i]; - if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + if (!smoothingEnabled) { - continue; + const std::uint16_t& dataValue = dataMomentsArray16[i]; + if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + { + continue; + } + + for (std::size_t m = 0; m < vertexCount; m++) + { + dataMoments16[mIndex++] = dataValue; + } } + else if (gate > 0) + { + // Validate indices are all in range + if (i + 1 >= numberOfDataMomentGates || + i + 1 >= numberOfNextDataMomentGates) + { + continue; + } + + const std::uint16_t& dm1 = dataMomentsArray16[i]; + const std::uint16_t& dm2 = dataMomentsArray16[i + 1]; + const std::uint16_t& dm3 = nextDataMomentsArray16[i]; + const std::uint16_t& dm4 = nextDataMomentsArray16[i + 1]; + + if ((!showSmoothedRangeFolding && // + (dm1 < snrThreshold || dm1 == RANGE_FOLDED) && + (dm2 < snrThreshold || dm2 == RANGE_FOLDED) && + (dm3 < snrThreshold || dm3 == RANGE_FOLDED) && + (dm4 < snrThreshold || dm4 == RANGE_FOLDED)) || + (showSmoothedRangeFolding && // + dm1 < snrThreshold && dm1 != RANGE_FOLDED && + dm2 < snrThreshold && dm2 != RANGE_FOLDED && + dm3 < snrThreshold && dm3 != RANGE_FOLDED && + dm4 < snrThreshold && dm4 != RANGE_FOLDED)) + { + // Skip only if all data moments are hidden + continue; + } - for (std::size_t m = 0; m < vertexCount; m++) + // The order must match the store vertices section below + dataMoments16[mIndex++] = p->RemapDataMoment(dm1); + dataMoments16[mIndex++] = p->RemapDataMoment(dm2); + dataMoments16[mIndex++] = p->RemapDataMoment(dm4); + dataMoments16[mIndex++] = p->RemapDataMoment(dm1); + dataMoments16[mIndex++] = p->RemapDataMoment(dm3); + dataMoments16[mIndex++] = p->RemapDataMoment(dm4); + + // cfpMoments is unused, so not populated here + } + else { - dataMoments16[mIndex++] = dataMomentsArray16[i]; + // If smoothing is enabled, gate should never start at zero + // (radar site origin) + continue; } } + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) + // Store vertices if (gate > 0) { + // Draw two triangles per gate + // + // 2 +---+ 4 + // | /| + // | / | + // |/ | + // 1 +---+ 3 + const std::uint16_t baseCoord = gate - 1; - std::size_t offset1 = ((startRadial + radial) % vertexRadials * - common::MAX_DATA_MOMENT_GATES + - baseCoord) * - 2; - std::size_t offset2 = offset1 + gateSize * 2; - std::size_t offset3 = + const std::size_t offset1 = + ((startRadial + radial) % vertexRadials * + common::MAX_DATA_MOMENT_GATES + + baseCoord) * + 2; + const std::size_t offset2 = + offset1 + static_cast(gateSize) * 2; + const std::size_t offset3 = (((startRadial + radial + 1) % vertexRadials) * common::MAX_DATA_MOMENT_GATES + baseCoord) * 2; - std::size_t offset4 = offset3 + gateSize * 2; + const std::size_t offset4 = + offset3 + static_cast(gateSize) * 2; vertices[vIndex++] = coordinates[offset1]; vertices[vIndex++] = coordinates[offset1 + 1]; @@ -750,19 +954,17 @@ void Level2ProductView::ComputeSweep() vertices[vIndex++] = coordinates[offset2]; vertices[vIndex++] = coordinates[offset2 + 1]; - vertices[vIndex++] = coordinates[offset3]; - vertices[vIndex++] = coordinates[offset3 + 1]; + vertices[vIndex++] = coordinates[offset4]; + vertices[vIndex++] = coordinates[offset4 + 1]; + + vertices[vIndex++] = coordinates[offset1]; + vertices[vIndex++] = coordinates[offset1 + 1]; vertices[vIndex++] = coordinates[offset3]; vertices[vIndex++] = coordinates[offset3 + 1]; vertices[vIndex++] = coordinates[offset4]; vertices[vIndex++] = coordinates[offset4 + 1]; - - vertices[vIndex++] = coordinates[offset2]; - vertices[vIndex++] = coordinates[offset2 + 1]; - - vertexCount = 6; } else { @@ -786,8 +988,6 @@ void Level2ProductView::ComputeSweep() vertices[vIndex++] = coordinates[offset2]; vertices[vIndex++] = coordinates[offset2 + 1]; - - vertexCount = 3; } } } @@ -819,8 +1019,50 @@ void Level2ProductView::ComputeSweep() Q_EMIT SweepComputed(); } -void Level2ProductViewImpl::ComputeCoordinates( - const std::shared_ptr& radarData) +void Level2ProductView::Impl::ComputeEdgeValue() +{ + const float offset = momentDataBlock0_->offset(); + + switch (dataBlockType_) + { + case wsr88d::rda::DataBlockType::MomentVel: + case wsr88d::rda::DataBlockType::MomentZdr: + edgeValue_ = static_cast(offset); + break; + + case wsr88d::rda::DataBlockType::MomentSw: + case wsr88d::rda::DataBlockType::MomentPhi: + edgeValue_ = 2; + break; + + case wsr88d::rda::DataBlockType::MomentRho: + edgeValue_ = std::numeric_limits::max(); + break; + + case wsr88d::rda::DataBlockType::MomentRef: + default: + edgeValue_ = 0; + break; + } +} + +template +T Level2ProductView::Impl::RemapDataMoment(T dataMoment) const +{ + if (dataMoment != 0 && + (dataMoment != RANGE_FOLDED || showSmoothedRangeFolding_)) + { + return dataMoment; + } + else + { + return edgeValue_; + } +} + +void Level2ProductView::Impl::ComputeCoordinates( + const std::shared_ptr& radarData, + bool smoothingEnabled) { logger_->debug("ComputeCoordinates()"); @@ -860,6 +1102,14 @@ void Level2ProductViewImpl::ComputeCoordinates( auto radials = boost::irange(0u, numRadials); auto gates = boost::irange(0u, numRangeBins); + const float gateRangeOffset = (smoothingEnabled) ? + // Center of the first gate is half the gate + // size distance from the radar site + 0.5f : + // Far end of the first gate is the gate + // size distance from the radar site + 1.0f; + std::for_each( std::execution::par_unseq, radials.begin(), @@ -869,7 +1119,7 @@ void Level2ProductViewImpl::ComputeCoordinates( units::degrees angle {}; auto radialData = radarData->find(radial); - if (radialData != radarData->cend()) + if (radialData != radarData->cend() && !smoothingEnabled) { angle = radialData->second->azimuth_angle(); } @@ -880,19 +1130,60 @@ void Level2ProductViewImpl::ComputeCoordinates( auto prevRadial2 = radarData->find( (radial >= 2) ? radial - 2 : numRadials - (2 - radial)); - if (prevRadial1 != radarData->cend() && - prevRadial2 != radarData->cend()) + if (radialData != radarData->cend() && + prevRadial1 != radarData->cend() && smoothingEnabled) + { + const units::degrees currentAngle = + radialData->second->azimuth_angle(); + const units::degrees prevAngle = + prevRadial1->second->azimuth_angle(); + + // Calculate delta angle + const units::degrees deltaAngle = + NormalizeAngle(currentAngle - prevAngle); + + // Delta scale is half the delta angle to reach the center of the + // bin, because smoothing is enabled + constexpr float deltaScale = 0.5f; + + angle = currentAngle + deltaAngle * deltaScale; + } + else if (radialData != radarData->cend() && smoothingEnabled) + { + const units::degrees currentAngle = + radialData->second->azimuth_angle(); + + // Assume a half degree delta if there aren't enough angles + // to determine a delta angle + constexpr units::degrees deltaAngle {0.5f}; + + // Delta scale is half the delta angle to reach the center of the + // bin, because smoothing is enabled + constexpr float deltaScale = 0.5f; + + angle = currentAngle + deltaAngle * deltaScale; + } + else if (prevRadial1 != radarData->cend() && + prevRadial2 != radarData->cend()) { const units::degrees prevAngle1 = prevRadial1->second->azimuth_angle(); const units::degrees prevAngle2 = prevRadial2->second->azimuth_angle(); - // No wrapping required since angle is only used for geodesic - // calculation - const units::degrees deltaAngle = prevAngle1 - prevAngle2; + // Calculate delta angle + const units::degrees deltaAngle = + NormalizeAngle(prevAngle1 - prevAngle2); - angle = prevAngle1 + deltaAngle; + const float deltaScale = + (smoothingEnabled) ? + // Delta scale is 1.5x the delta angle to reach the center + // of the next bin, because smoothing is enabled + 1.5f : + // Delta scale is 1.0x the delta angle + 1.0f; + + angle = prevAngle1 + deltaAngle * deltaScale; } else if (prevRadial1 != radarData->cend()) { @@ -903,7 +1194,15 @@ void Level2ProductViewImpl::ComputeCoordinates( // to determine a delta angle constexpr units::degrees deltaAngle {0.5f}; - angle = prevAngle1 + deltaAngle; + const float deltaScale = + (smoothingEnabled) ? + // Delta scale is 1.5x the delta angle to reach the center + // of the next bin, because smoothing is enabled + 1.5f : + // Delta scale is 1.0x the delta angle + 1.0f; + + angle = prevAngle1 + deltaAngle * deltaScale; } else { @@ -912,35 +1211,38 @@ void Level2ProductViewImpl::ComputeCoordinates( } } - std::for_each(std::execution::par_unseq, - gates.begin(), - gates.end(), - [&](std::uint32_t gate) - { - const std::uint32_t radialGate = - radial * common::MAX_DATA_MOMENT_GATES + gate; - const float range = (gate + 1) * gateSize; - const std::size_t offset = radialGate * 2; - - double latitude; - double longitude; - - geodesic.Direct(radarLatitude, - radarLongitude, - angle.value(), - range, - latitude, - longitude); - - coordinates_[offset] = latitude; - coordinates_[offset + 1] = longitude; - }); + std::for_each( + std::execution::par_unseq, + gates.begin(), + gates.end(), + [&](std::uint32_t gate) + { + const std::uint32_t radialGate = + radial * common::MAX_DATA_MOMENT_GATES + gate; + const float range = + (static_cast(gate) + gateRangeOffset) * gateSize; + const std::size_t offset = + static_cast(radialGate) * 2; + + double latitude = 0.0; + double longitude = 0.0; + + geodesic.Direct(radarLatitude, + radarLongitude, + angle.value(), + range, + latitude, + longitude); + + coordinates_[offset] = static_cast(latitude); + coordinates_[offset + 1] = static_cast(longitude); + }); }); timer.stop(); logger_->debug("Coordinates calculated in {}", timer.format(6, "%ws")); } -bool Level2ProductViewImpl::IsRadarDataIncomplete( +bool Level2ProductView::Impl::IsRadarDataIncomplete( const std::shared_ptr& radarData) { // Assume the data is incomplete when the delta between the first and last @@ -957,6 +1259,25 @@ bool Level2ProductViewImpl::IsRadarDataIncomplete( return angleDelta > kIncompleteDataAngleThreshold_; } +units::degrees +Level2ProductView::Impl::NormalizeAngle(units::degrees angle) +{ + constexpr auto angleLimit = units::degrees {180.0f}; + constexpr auto fullAngle = units::degrees {360.0f}; + + // Normalize angle to [-180, 180) + while (angle < -angleLimit) + { + angle += fullAngle; + } + while (angle >= angleLimit) + { + angle -= fullAngle; + } + + return angle; +} + std::optional Level2ProductView::GetBinLevel(const common::Coordinate& coordinate) const { @@ -1003,7 +1324,7 @@ Level2ProductView::GetBinLevel(const common::Coordinate& coordinate) const static_cast(radarData->crbegin()->first + 1); // Add an extra radial when incomplete data exists - if (Level2ProductViewImpl::IsRadarDataIncomplete(radarData)) + if (Impl::IsRadarDataIncomplete(radarData)) { ++numRadials; } diff --git a/scwx-qt/source/scwx/qt/view/level2_product_view.hpp b/scwx-qt/source/scwx/qt/view/level2_product_view.hpp index 9e25a254..db8fc45c 100644 --- a/scwx-qt/source/scwx/qt/view/level2_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/level2_product_view.hpp @@ -15,8 +15,6 @@ namespace qt namespace view { -class Level2ProductViewImpl; - class Level2ProductView : public RadarProductView { Q_OBJECT @@ -73,7 +71,8 @@ protected slots: void ComputeSweep() override; private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace 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 d0bb0d47..3858ac11 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.cpp @@ -485,6 +485,52 @@ void Level3ProductView::UpdateColorTableLut() Q_EMIT ColorTableLutUpdated(); } +std::uint8_t Level3ProductView::ComputeEdgeValue() const +{ + std::uint8_t edgeValue = 0; + + const std::shared_ptr + descriptionBlock = p->graphicMessage_->description_block(); + + const float offset = descriptionBlock->offset(); + const float scale = descriptionBlock->scale(); + + switch (p->category_) + { + case common::Level3ProductCategory::Velocity: + edgeValue = static_cast((scale > 0.0f) ? (-offset / scale) : + -offset); + break; + + case common::Level3ProductCategory::DifferentialReflectivity: + edgeValue = static_cast(-offset); + break; + + case common::Level3ProductCategory::SpectrumWidth: + case common::Level3ProductCategory::SpecificDifferentialPhase: + edgeValue = 2; + break; + + case common::Level3ProductCategory::CorrelationCoefficient: + edgeValue = static_cast( + std::max(std::numeric_limits::max(), + descriptionBlock->number_of_levels())); + break; + + case common::Level3ProductCategory::Reflectivity: + case common::Level3ProductCategory::StormRelativeVelocity: + case common::Level3ProductCategory::VerticallyIntegratedLiquid: + case common::Level3ProductCategory::EchoTops: + case common::Level3ProductCategory::HydrometeorClassification: + case common::Level3ProductCategory::PrecipitationAccumulation: + default: + edgeValue = 0; + break; + } + + return edgeValue; +} + std::optional Level3ProductView::GetDataLevelCode(std::uint16_t level) const { diff --git a/scwx-qt/source/scwx/qt/view/level3_product_view.hpp b/scwx-qt/source/scwx/qt/view/level3_product_view.hpp index e836c6e0..b5e043b3 100644 --- a/scwx-qt/source/scwx/qt/view/level3_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/level3_product_view.hpp @@ -58,6 +58,8 @@ class Level3ProductView : public RadarProductView void DisconnectRadarProductManager() override; void UpdateColorTableLut() override; + [[nodiscard]] std::uint8_t ComputeEdgeValue() const; + private: class Impl; std::unique_ptr p; 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 5fa3531f..135f5e65 100644 --- a/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_radial_view.cpp @@ -44,7 +44,11 @@ class Level3RadialView::Impl ~Impl() { threadPool_.join(); }; void ComputeCoordinates( - const std::shared_ptr& radialData); + const std::shared_ptr& radialData, + bool smoothingEnabled); + + [[nodiscard]] inline std::uint8_t + RemapDataMoment(std::uint8_t dataMoment) const; Level3RadialView* self_; @@ -53,8 +57,13 @@ class Level3RadialView::Impl std::vector coordinates_ {}; std::vector vertices_ {}; std::vector dataMoments8_ {}; + std::uint8_t edgeValue_ {}; + + bool showSmoothedRangeFolding_ {false}; std::shared_ptr lastRadialData_ {}; + bool lastShowSmoothedRangeFolding_ {false}; + bool lastSmoothingEnabled_ {false}; float latitude_; float longitude_; @@ -125,6 +134,9 @@ void Level3RadialView::ComputeSweep() std::shared_ptr radarProductManager = radar_product_manager(); + const bool smoothingEnabled = smoothing_enabled(); + p->showSmoothedRangeFolding_ = show_smoothed_range_folding(); + const bool& showSmoothedRangeFolding = p->showSmoothedRangeFolding_; // Retrieve message from Radar Product Manager std::shared_ptr message; @@ -155,7 +167,10 @@ void Level3RadialView::ComputeSweep() Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } - else if (gpm == graphic_product_message()) + else if (gpm == graphic_product_message() && + smoothingEnabled == p->lastSmoothingEnabled_ && + (showSmoothedRangeFolding == p->lastShowSmoothedRangeFolding_ || + !smoothingEnabled)) { // Skip if this is the message we previously processed Q_EMIT SweepNotComputed(types::NoUpdateReason::NoChange); @@ -163,6 +178,9 @@ void Level3RadialView::ComputeSweep() } set_graphic_product_message(gpm); + p->lastShowSmoothedRangeFolding_ = showSmoothedRangeFolding; + p->lastSmoothingEnabled_ = smoothingEnabled; + // A message with radial data should have a Product Description Block and // Product Symbology Block std::shared_ptr descriptionBlock = @@ -267,11 +285,11 @@ void Level3RadialView::ComputeSweep() const std::vector& coordinates = (radialSize == common::RadialSize::NonStandard) ? p->coordinates_ : - radarProductManager->coordinates(radialSize); + radarProductManager->coordinates(radialSize, smoothingEnabled); // There should be a positive number of range bins in radial data - const uint16_t gates = radialData->number_of_range_bins(); - if (gates < 1) + const uint16_t numberOfDataMomentGates = radialData->number_of_range_bins(); + if (numberOfDataMomentGates < 1) { logger_->warn("No range bins in radial data"); Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); @@ -293,13 +311,14 @@ void Level3RadialView::ComputeSweep() std::vector& vertices = p->vertices_; size_t vIndex = 0; vertices.clear(); - vertices.resize(radials * gates * VERTICES_PER_BIN * VALUES_PER_VERTEX); + vertices.resize(radials * numberOfDataMomentGates * VERTICES_PER_BIN * + VALUES_PER_VERTEX); // Setup data moment vector std::vector& dataMoments8 = p->dataMoments8_; size_t mIndex = 0; - dataMoments8.resize(radials * gates * VERTICES_PER_BIN); + dataMoments8.resize(radials * numberOfDataMomentGates * VERTICES_PER_BIN); // Compute threshold at which to display an individual bin const uint16_t snrThreshold = descriptionBlock->threshold(); @@ -308,7 +327,7 @@ void Level3RadialView::ComputeSweep() std::uint16_t startRadial; if (radialSize == common::RadialSize::NonStandard) { - p->ComputeCoordinates(radialData); + p->ComputeCoordinates(radialData, smoothingEnabled); startRadial = 0; } else @@ -318,40 +337,105 @@ void Level3RadialView::ComputeSweep() startRadial = std::lroundf(startAngle * radialMultiplier); } - for (uint16_t radial = 0; radial < radialData->number_of_radials(); radial++) - { - const auto dataMomentsArray8 = radialData->level(radial); + // Compute gate interval + const std::uint16_t dataMomentInterval = + descriptionBlock->x_resolution_raw(); - // Compute gate interval - const 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())); - // Compute gate size (number of base gates per bin) - const uint16_t gateSize = std::max( - 1, - dataMomentInterval / - static_cast(radarProductManager->gate_size())); + // Compute gate range [startGate, endGate) + std::uint16_t startGate = 0; + const std::uint16_t endGate = + std::min(startGate + numberOfDataMomentGates * gateSize, + common::MAX_DATA_MOMENT_GATES); - // Compute gate range [startGate, endGate) - const uint16_t startGate = 0; - const uint16_t endGate = std::min( - startGate + gates * gateSize, common::MAX_DATA_MOMENT_GATES); + if (smoothingEnabled) + { + // If smoothing is enabled, the start gate is incremented by one, as we + // are skipping the radar site origin. The end gate is unaffected, as + // we need to draw one less data point. + ++startGate; + + // For most products other than reflectivity, the edge should not go to + // the bottom of the color table + p->edgeValue_ = ComputeEdgeValue(); + } + + for (std::uint16_t radial = 0; radial < radialData->number_of_radials(); + ++radial) + { + const auto& dataMomentsArray8 = radialData->level(radial); + + const std::uint16_t nextRadial = + (radial == radialData->number_of_radials() - 1) ? 0 : radial + 1; + const auto& nextDataMomentsArray8 = radialData->level(nextRadial); - for (uint16_t gate = startGate, i = 0; gate + gateSize <= endGate; + for (std::uint16_t gate = startGate, i = 0; gate + gateSize <= endGate; gate += gateSize, ++i) { size_t vertexCount = (gate > 0) ? 6 : 3; - // Store data moment value - uint8_t dataValue = - (i < dataMomentsArray8.size()) ? dataMomentsArray8[i] : 0; - if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + if (!smoothingEnabled) { - continue; + // Store data moment value + const uint8_t dataValue = + (i < dataMomentsArray8.size()) ? dataMomentsArray8[i] : 0; + if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + { + continue; + } + + for (size_t m = 0; m < vertexCount; m++) + { + dataMoments8[mIndex++] = dataValue; + } } + else if (gate > 0) + { + // Validate indices are all in range + if (i + 1 >= numberOfDataMomentGates) + { + continue; + } + + const std::uint8_t& dm1 = dataMomentsArray8[i]; + const std::uint8_t& dm2 = dataMomentsArray8[i + 1]; + const std::uint8_t& dm3 = nextDataMomentsArray8[i]; + const std::uint8_t& dm4 = nextDataMomentsArray8[i + 1]; + + if ((!showSmoothedRangeFolding && // + (dm1 < snrThreshold || dm1 == RANGE_FOLDED) && + (dm2 < snrThreshold || dm2 == RANGE_FOLDED) && + (dm3 < snrThreshold || dm3 == RANGE_FOLDED) && + (dm4 < snrThreshold || dm4 == RANGE_FOLDED)) || + (showSmoothedRangeFolding && // + dm1 < snrThreshold && dm1 != RANGE_FOLDED && + dm2 < snrThreshold && dm2 != RANGE_FOLDED && + dm3 < snrThreshold && dm3 != RANGE_FOLDED && + dm4 < snrThreshold && dm4 != RANGE_FOLDED)) + { + // Skip only if all data moments are hidden + continue; + } - for (size_t m = 0; m < vertexCount; m++) + // The order must match the store vertices section below + dataMoments8[mIndex++] = p->RemapDataMoment(dm1); + dataMoments8[mIndex++] = p->RemapDataMoment(dm2); + dataMoments8[mIndex++] = p->RemapDataMoment(dm4); + dataMoments8[mIndex++] = p->RemapDataMoment(dm1); + dataMoments8[mIndex++] = p->RemapDataMoment(dm3); + dataMoments8[mIndex++] = p->RemapDataMoment(dm4); + } + else { - dataMoments8[mIndex++] = dataValue; + // If smoothing is enabled, gate should never start at zero + // (radar site origin) + logger_->error("Smoothing enabled, gate should not start at zero"); + continue; } // Store vertices @@ -376,19 +460,17 @@ void Level3RadialView::ComputeSweep() vertices[vIndex++] = coordinates[offset2]; vertices[vIndex++] = coordinates[offset2 + 1]; - vertices[vIndex++] = coordinates[offset3]; - vertices[vIndex++] = coordinates[offset3 + 1]; + vertices[vIndex++] = coordinates[offset4]; + vertices[vIndex++] = coordinates[offset4 + 1]; + + vertices[vIndex++] = coordinates[offset1]; + vertices[vIndex++] = coordinates[offset1 + 1]; vertices[vIndex++] = coordinates[offset3]; vertices[vIndex++] = coordinates[offset3 + 1]; vertices[vIndex++] = coordinates[offset4]; vertices[vIndex++] = coordinates[offset4 + 1]; - - vertices[vIndex++] = coordinates[offset2]; - vertices[vIndex++] = coordinates[offset2 + 1]; - - vertexCount = 6; } else { @@ -411,8 +493,6 @@ void Level3RadialView::ComputeSweep() vertices[vIndex++] = coordinates[offset2]; vertices[vIndex++] = coordinates[offset2 + 1]; - - vertexCount = 3; } } } @@ -430,8 +510,23 @@ void Level3RadialView::ComputeSweep() Q_EMIT SweepComputed(); } +std::uint8_t +Level3RadialView::Impl::RemapDataMoment(std::uint8_t dataMoment) const +{ + if (dataMoment != 0 && + (dataMoment != RANGE_FOLDED || showSmoothedRangeFolding_)) + { + return dataMoment; + } + else + { + return edgeValue_; + } +} + void Level3RadialView::Impl::ComputeCoordinates( - const std::shared_ptr& radialData) + const std::shared_ptr& radialData, + bool smoothingEnabled) { logger_->debug("ComputeCoordinates()"); @@ -455,38 +550,54 @@ void Level3RadialView::Impl::ComputeCoordinates( auto radials = boost::irange(0u, numRadials); auto gates = boost::irange(0u, numRangeBins); - std::for_each(std::execution::par_unseq, - radials.begin(), - radials.end(), - [&](std::uint32_t radial) - { - const float angle = radialData->start_angle(radial); - - std::for_each(std::execution::par_unseq, - gates.begin(), - gates.end(), - [&](std::uint32_t gate) - { - const std::uint32_t radialGate = - radial * common::MAX_DATA_MOMENT_GATES + - gate; - const float range = (gate + 1) * gateSize; - const std::size_t offset = radialGate * 2; - - double latitude; - double longitude; - - geodesic.Direct(radarLatitude, - radarLongitude, - angle, - range, - latitude, - longitude); - - coordinates_[offset] = latitude; - coordinates_[offset + 1] = longitude; - }); - }); + const float gateRangeOffset = (smoothingEnabled) ? + // Center of the first gate is half the gate + // size distance from the radar site + 0.5f : + // Far end of the first gate is the gate + // size distance from the radar site + 1.0f; + + std::for_each( + std::execution::par_unseq, + radials.begin(), + radials.end(), + [&](std::uint32_t radial) + { + float angle = radialData->start_angle(radial); + + if (smoothingEnabled) + { + static constexpr float kDeltaAngleFactor = 0.5f; + angle += radialData->delta_angle(radial) * kDeltaAngleFactor; + } + + std::for_each( + std::execution::par_unseq, + gates.begin(), + gates.end(), + [&](std::uint32_t gate) + { + const std::uint32_t radialGate = + radial * common::MAX_DATA_MOMENT_GATES + gate; + const float range = + (static_cast(gate) + gateRangeOffset) * gateSize; + const std::size_t offset = static_cast(radialGate) * 2; + + double latitude = 0.0; + double longitude = 0.0; + + geodesic.Direct(radarLatitude, + radarLongitude, + angle, + range, + latitude, + longitude); + + coordinates_[offset] = static_cast(latitude); + coordinates_[offset + 1] = static_cast(longitude); + }); + }); timer.stop(); logger_->debug("Coordinates calculated in {}", timer.format(6, "%ws")); } 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 b51c2cd0..3056cc03 100644 --- a/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp +++ b/scwx-qt/source/scwx/qt/view/level3_raster_view.cpp @@ -33,12 +33,20 @@ class Level3RasterViewImpl } ~Level3RasterViewImpl() { threadPool_.join(); }; + [[nodiscard]] inline std::uint8_t + RemapDataMoment(std::uint8_t dataMoment) const; + boost::asio::thread_pool threadPool_ {1u}; - std::vector vertices_; - std::vector dataMoments8_; + std::vector vertices_ {}; + std::vector dataMoments8_ {}; + std::uint8_t edgeValue_ {}; + + bool showSmoothedRangeFolding_ {false}; std::shared_ptr lastRasterData_ {}; + bool lastShowSmoothedRangeFolding_ {false}; + bool lastSmoothingEnabled_ {false}; float latitude_; float longitude_; @@ -109,6 +117,9 @@ void Level3RasterView::ComputeSweep() std::shared_ptr radarProductManager = radar_product_manager(); + const bool smoothingEnabled = smoothing_enabled(); + p->showSmoothedRangeFolding_ = show_smoothed_range_folding(); + const bool& showSmoothedRangeFolding = p->showSmoothedRangeFolding_; // Retrieve message from Radar Product Manager std::shared_ptr message; @@ -139,7 +150,10 @@ void Level3RasterView::ComputeSweep() Q_EMIT SweepNotComputed(types::NoUpdateReason::InvalidData); return; } - else if (gpm == graphic_product_message()) + else if (gpm == graphic_product_message() && + smoothingEnabled == p->lastSmoothingEnabled_ && + (showSmoothedRangeFolding == p->lastShowSmoothedRangeFolding_ || + !smoothingEnabled)) { // Skip if this is the message we previously processed Q_EMIT SweepNotComputed(types::NoUpdateReason::NoChange); @@ -147,6 +161,9 @@ void Level3RasterView::ComputeSweep() } set_graphic_product_message(gpm); + p->lastShowSmoothedRangeFolding_ = showSmoothedRangeFolding; + p->lastSmoothingEnabled_ = smoothingEnabled; + // A message with radial data should have a Product Description Block and // Product Symbology Block std::shared_ptr descriptionBlock = @@ -231,16 +248,18 @@ void Level3RasterView::ComputeSweep() const GeographicLib::Geodesic& geodesic = util::GeographicLib::DefaultGeodesic(); - const uint16_t xResolution = descriptionBlock->x_resolution_raw(); - const uint16_t yResolution = descriptionBlock->y_resolution_raw(); - double iCoordinate = + const std::uint16_t xResolution = descriptionBlock->x_resolution_raw(); + const std::uint16_t yResolution = descriptionBlock->y_resolution_raw(); + const double iCoordinate = (-rasterData->i_coordinate_start() - 1.0 - p->range_) * 1000.0; - double jCoordinate = + const double jCoordinate = (rasterData->j_coordinate_start() + 1.0 + p->range_) * 1000.0; + const double xOffset = (smoothingEnabled) ? xResolution * 0.5 : 0.0; + const double yOffset = (smoothingEnabled) ? yResolution * 0.5 : 0.0; - size_t numCoordinates = + const std::size_t numCoordinates = static_cast(rows + 1) * static_cast(maxColumns + 1); - auto coordinateRange = + const auto coordinateRange = boost::irange(0, static_cast(numCoordinates)); std::vector coordinates; @@ -260,8 +279,8 @@ void Level3RasterView::ComputeSweep() const uint32_t col = index % (rows + 1); const uint32_t row = index / (rows + 1); - const double i = iCoordinate + xResolution * col; - const double j = jCoordinate - yResolution * row; + const double i = iCoordinate + xResolution * col + xOffset; + const double j = jCoordinate - yResolution * row - yOffset; // Calculate polar coordinates based on i and j const double angle = std::atan2(i, j) * 180.0 / M_PI; @@ -299,25 +318,83 @@ void Level3RasterView::ComputeSweep() // Compute threshold at which to display an individual bin const uint16_t snrThreshold = descriptionBlock->threshold(); - for (size_t row = 0; row < rasterData->number_of_rows(); ++row) + const std::size_t rowCount = (smoothingEnabled) ? + rasterData->number_of_rows() - 1 : + rasterData->number_of_rows(); + + if (smoothingEnabled) + { + // For most products other than reflectivity, the edge should not go to + // the bottom of the color table + p->edgeValue_ = ComputeEdgeValue(); + } + + for (std::size_t row = 0; row < rowCount; ++row) { - const auto dataMomentsArray8 = + const std::size_t nextRow = + (row == static_cast(rasterData->number_of_rows() - 1)) ? + 0 : + row + 1; + + const auto& dataMomentsArray8 = rasterData->level(static_cast(row)); + const auto& nextDataMomentsArray8 = + rasterData->level(static_cast(nextRow)); for (size_t bin = 0; bin < dataMomentsArray8.size(); ++bin) { - constexpr size_t vertexCount = 6; - - // Store data moment value - uint8_t dataValue = dataMomentsArray8[bin]; - if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + if (!smoothingEnabled) { - continue; + static constexpr std::size_t vertexCount = 6; + + // Store data moment value + const std::uint8_t& dataValue = dataMomentsArray8[bin]; + if (dataValue < snrThreshold && dataValue != RANGE_FOLDED) + { + continue; + } + + for (size_t m = 0; m < vertexCount; m++) + { + dataMoments8[mIndex++] = dataValue; + } } - - for (size_t m = 0; m < vertexCount; m++) + else { - dataMoments8[mIndex++] = dataValue; + // Validate indices are all in range + if (bin + 1 >= dataMomentsArray8.size() || + bin + 1 >= nextDataMomentsArray8.size()) + { + continue; + } + + const std::uint8_t& dm1 = dataMomentsArray8[bin]; + const std::uint8_t& dm2 = dataMomentsArray8[bin + 1]; + const std::uint8_t& dm3 = nextDataMomentsArray8[bin]; + const std::uint8_t& dm4 = nextDataMomentsArray8[bin + 1]; + + if ((!showSmoothedRangeFolding && // + (dm1 < snrThreshold || dm1 == RANGE_FOLDED) && + (dm2 < snrThreshold || dm2 == RANGE_FOLDED) && + (dm3 < snrThreshold || dm3 == RANGE_FOLDED) && + (dm4 < snrThreshold || dm4 == RANGE_FOLDED)) || + (showSmoothedRangeFolding && // + dm1 < snrThreshold && dm1 != RANGE_FOLDED && + dm2 < snrThreshold && dm2 != RANGE_FOLDED && + dm3 < snrThreshold && dm3 != RANGE_FOLDED && + dm4 < snrThreshold && dm4 != RANGE_FOLDED)) + { + // Skip only if all data moments are hidden + continue; + } + + // The order must match the store vertices section below + dataMoments8[mIndex++] = p->RemapDataMoment(dm1); + dataMoments8[mIndex++] = p->RemapDataMoment(dm2); + dataMoments8[mIndex++] = p->RemapDataMoment(dm4); + dataMoments8[mIndex++] = p->RemapDataMoment(dm1); + dataMoments8[mIndex++] = p->RemapDataMoment(dm3); + dataMoments8[mIndex++] = p->RemapDataMoment(dm4); } // Store vertices @@ -332,17 +409,17 @@ void Level3RasterView::ComputeSweep() vertices[vIndex++] = coordinates[offset2]; vertices[vIndex++] = coordinates[offset2 + 1]; - vertices[vIndex++] = coordinates[offset3]; - vertices[vIndex++] = coordinates[offset3 + 1]; + vertices[vIndex++] = coordinates[offset4]; + vertices[vIndex++] = coordinates[offset4 + 1]; + + vertices[vIndex++] = coordinates[offset1]; + vertices[vIndex++] = coordinates[offset1 + 1]; vertices[vIndex++] = coordinates[offset3]; vertices[vIndex++] = coordinates[offset3 + 1]; vertices[vIndex++] = coordinates[offset4]; vertices[vIndex++] = coordinates[offset4 + 1]; - - vertices[vIndex++] = coordinates[offset2]; - vertices[vIndex++] = coordinates[offset2 + 1]; } } vertices.resize(vIndex); @@ -359,6 +436,20 @@ void Level3RasterView::ComputeSweep() Q_EMIT SweepComputed(); } +std::uint8_t +Level3RasterViewImpl::RemapDataMoment(std::uint8_t dataMoment) const +{ + if (dataMoment != 0 && + (dataMoment != RANGE_FOLDED || showSmoothedRangeFolding_)) + { + return dataMoment; + } + else + { + return edgeValue_; + } +} + std::optional Level3RasterView::GetBinLevel(const common::Coordinate& coordinate) const { 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 e2ca6c21..9c5a84de 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -28,26 +29,44 @@ class RadarProductViewImpl { public: explicit RadarProductViewImpl( + RadarProductView* self, std::shared_ptr radarProductManager) : + self_ {self}, initialized_ {false}, sweepMutex_ {}, selectedTime_ {}, radarProductManager_ {radarProductManager} { + auto& productSettings = settings::ProductSettings::Instance(); + connection_ = productSettings.changed_signal().connect( + [this]() + { + showSmoothedRangeFolding_ = settings::ProductSettings::Instance() + .show_smoothed_range_folding() + .GetValue(); + self_->Update(); + }); + ; } ~RadarProductViewImpl() {} + RadarProductView* self_; + bool initialized_; std::mutex sweepMutex_; std::chrono::system_clock::time_point selectedTime_; + bool showSmoothedRangeFolding_ {false}; + bool smoothingEnabled_ {false}; std::shared_ptr radarProductManager_; + + boost::signals2::scoped_connection connection_; }; RadarProductView::RadarProductView( std::shared_ptr radarProductManager) : - p(std::make_unique(radarProductManager)) {}; + p(std::make_unique(this, radarProductManager)) {}; RadarProductView::~RadarProductView() = default; const std::vector& @@ -87,6 +106,16 @@ std::chrono::system_clock::time_point RadarProductView::selected_time() const return p->selectedTime_; } +bool RadarProductView::show_smoothed_range_folding() const +{ + return p->showSmoothedRangeFolding_; +} + +bool RadarProductView::smoothing_enabled() const +{ + return p->smoothingEnabled_; +} + std::chrono::system_clock::time_point RadarProductView::sweep_time() const { return {}; @@ -105,6 +134,11 @@ void RadarProductView::set_radar_product_manager( ConnectRadarProductManager(); } +void RadarProductView::set_smoothing_enabled(bool smoothingEnabled) +{ + p->smoothingEnabled_ = smoothingEnabled; +} + void RadarProductView::Initialize() { ComputeSweep(); 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 c695a9e5..31d47840 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp @@ -47,12 +47,16 @@ class RadarProductView : public QObject virtual std::uint16_t vcp() const = 0; virtual const std::vector& vertices() const = 0; - std::shared_ptr radar_product_manager() const; - std::chrono::system_clock::time_point selected_time() const; - std::mutex& sweep_mutex(); + [[nodiscard]] std::shared_ptr + radar_product_manager() const; + [[nodiscard]] std::chrono::system_clock::time_point selected_time() const; + [[nodiscard]] bool show_smoothed_range_folding() const; + [[nodiscard]] bool smoothing_enabled() const; + [[nodiscard]] std::mutex& sweep_mutex(); void set_radar_product_manager( std::shared_ptr radarProductManager); + void set_smoothing_enabled(bool smoothingEnabled); void Initialize(); virtual void diff --git a/test/data b/test/data index eaf8f185..0eb47590 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit eaf8f185ce2b3a3248da1a4d6c8e2e9265638f15 +Subproject commit 0eb475909f9e64ce81e7b8b39420d980b81b3baa