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 @@
+
+
+ -
+
+
+ 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