From d92c1e50f25e38affb8f5bf6bc34ebae6db4f0e9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 3 Feb 2024 08:53:23 -0600 Subject: [PATCH 01/37] Refactor ScitForecastDataPacket to ScitDataPacket --- .../scwx/wsr88d/rpg/scit_data_packet.hpp | 43 +++++++++ .../wsr88d/rpg/scit_forecast_data_packet.hpp | 44 ---------- .../source/scwx/wsr88d/rpg/packet_factory.cpp | 6 +- .../scwx/wsr88d/rpg/scit_data_packet.cpp | 80 +++++++++++++++++ .../wsr88d/rpg/scit_forecast_data_packet.cpp | 88 ------------------- wxdata/wxdata.cmake | 4 +- 6 files changed, 128 insertions(+), 137 deletions(-) create mode 100644 wxdata/include/scwx/wsr88d/rpg/scit_data_packet.hpp delete mode 100644 wxdata/include/scwx/wsr88d/rpg/scit_forecast_data_packet.hpp create mode 100644 wxdata/source/scwx/wsr88d/rpg/scit_data_packet.cpp delete mode 100644 wxdata/source/scwx/wsr88d/rpg/scit_forecast_data_packet.cpp diff --git a/wxdata/include/scwx/wsr88d/rpg/scit_data_packet.hpp b/wxdata/include/scwx/wsr88d/rpg/scit_data_packet.hpp new file mode 100644 index 00000000..37aac08a --- /dev/null +++ b/wxdata/include/scwx/wsr88d/rpg/scit_data_packet.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include +#include + +namespace scwx +{ +namespace wsr88d +{ +namespace rpg +{ + +class ScitDataPacket : public SpecialGraphicSymbolPacket +{ +public: + explicit ScitDataPacket(); + ~ScitDataPacket(); + + ScitDataPacket(const ScitDataPacket&) = delete; + ScitDataPacket& operator=(const ScitDataPacket&) = delete; + + ScitDataPacket(ScitDataPacket&&) noexcept; + ScitDataPacket& operator=(ScitDataPacket&&) noexcept; + + const std::vector& data() const; + + std::size_t RecordCount() const override; + + static std::shared_ptr Create(std::istream& is); + +protected: + bool ParseData(std::istream& is) override; + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace rpg +} // namespace wsr88d +} // namespace scwx diff --git a/wxdata/include/scwx/wsr88d/rpg/scit_forecast_data_packet.hpp b/wxdata/include/scwx/wsr88d/rpg/scit_forecast_data_packet.hpp deleted file mode 100644 index 56061d7a..00000000 --- a/wxdata/include/scwx/wsr88d/rpg/scit_forecast_data_packet.hpp +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include - -#include -#include - -namespace scwx -{ -namespace wsr88d -{ -namespace rpg -{ - -class ScitForecastDataPacketImpl; - -class ScitForecastDataPacket : public SpecialGraphicSymbolPacket -{ -public: - explicit ScitForecastDataPacket(); - ~ScitForecastDataPacket(); - - ScitForecastDataPacket(const ScitForecastDataPacket&) = delete; - ScitForecastDataPacket& operator=(const ScitForecastDataPacket&) = delete; - - ScitForecastDataPacket(ScitForecastDataPacket&&) noexcept; - ScitForecastDataPacket& operator=(ScitForecastDataPacket&&) noexcept; - - const std::vector& data() const; - - size_t RecordCount() const override; - - static std::shared_ptr Create(std::istream& is); - -protected: - bool ParseData(std::istream& is) override; - -private: - std::unique_ptr p; -}; - -} // namespace rpg -} // namespace wsr88d -} // namespace scwx diff --git a/wxdata/source/scwx/wsr88d/rpg/packet_factory.cpp b/wxdata/source/scwx/wsr88d/rpg/packet_factory.cpp index 01f91339..e3cd034b 100644 --- a/wxdata/source/scwx/wsr88d/rpg/packet_factory.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/packet_factory.cpp @@ -15,7 +15,7 @@ #include #include #include -#include +#include #include #include #include @@ -63,8 +63,8 @@ static const std::unordered_map create_ { {20, PointFeatureSymbolPacket::Create}, {21, CellTrendDataPacket::Create}, {22, CellTrendVolumeScanTimes::Create}, - {23, ScitForecastDataPacket::Create}, - {24, ScitForecastDataPacket::Create}, + {23, ScitDataPacket::Create}, + {24, ScitDataPacket::Create}, {25, StiCircleSymbolPacket::Create}, {26, PointGraphicSymbolPacket::Create}, {28, GenericDataPacket::Create}, diff --git a/wxdata/source/scwx/wsr88d/rpg/scit_data_packet.cpp b/wxdata/source/scwx/wsr88d/rpg/scit_data_packet.cpp new file mode 100644 index 00000000..74327c81 --- /dev/null +++ b/wxdata/source/scwx/wsr88d/rpg/scit_data_packet.cpp @@ -0,0 +1,80 @@ +#include +#include + +#include +#include +#include + +namespace scwx +{ +namespace wsr88d +{ +namespace rpg +{ + +static const std::string logPrefix_ = "scwx::wsr88d::rpg::scit_data_packet"; +static const auto logger_ = util::Logger::Create(logPrefix_); + +static const std::set packetCodes_ = {23, 24}; + +class ScitDataPacket::Impl +{ +public: + explicit Impl() : data_ {}, recordCount_ {0} {} + ~Impl() = default; + + std::vector data_; + size_t recordCount_; +}; + +ScitDataPacket::ScitDataPacket() : p(std::make_unique()) {} +ScitDataPacket::~ScitDataPacket() = default; + +ScitDataPacket::ScitDataPacket(ScitDataPacket&&) noexcept = default; +ScitDataPacket& ScitDataPacket::operator=(ScitDataPacket&&) noexcept = default; + +const std::vector& ScitDataPacket::data() const +{ + return p->data_; +} + +size_t ScitDataPacket::RecordCount() const +{ + return p->recordCount_; +} + +bool ScitDataPacket::ParseData(std::istream& is) +{ + bool blockValid = true; + + if (!packetCodes_.contains(packet_code())) + { + logger_->warn("Invalid packet code: {}", packet_code()); + blockValid = false; + } + + if (blockValid) + { + p->recordCount_ = length_of_block(); + p->data_.resize(p->recordCount_); + is.read(reinterpret_cast(p->data_.data()), p->recordCount_); + } + + return blockValid; +} + +std::shared_ptr ScitDataPacket::Create(std::istream& is) +{ + std::shared_ptr packet = std::make_shared(); + + if (!packet->Parse(is)) + { + packet.reset(); + } + + return packet; +} + +} // namespace rpg +} // namespace wsr88d +} // namespace scwx diff --git a/wxdata/source/scwx/wsr88d/rpg/scit_forecast_data_packet.cpp b/wxdata/source/scwx/wsr88d/rpg/scit_forecast_data_packet.cpp deleted file mode 100644 index 86899bec..00000000 --- a/wxdata/source/scwx/wsr88d/rpg/scit_forecast_data_packet.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include -#include - -#include -#include -#include - -namespace scwx -{ -namespace wsr88d -{ -namespace rpg -{ - -static const std::string logPrefix_ = - "scwx::wsr88d::rpg::scit_forecast_data_packet"; -static const auto logger_ = util::Logger::Create(logPrefix_); - -static const std::set packetCodes_ = {23, 24}; - -class ScitForecastDataPacketImpl -{ -public: - explicit ScitForecastDataPacketImpl() : data_ {}, recordCount_ {0} {} - ~ScitForecastDataPacketImpl() = default; - - std::vector data_; - size_t recordCount_; -}; - -ScitForecastDataPacket::ScitForecastDataPacket() : - p(std::make_unique()) -{ -} -ScitForecastDataPacket::~ScitForecastDataPacket() = default; - -ScitForecastDataPacket::ScitForecastDataPacket( - ScitForecastDataPacket&&) noexcept = default; -ScitForecastDataPacket& -ScitForecastDataPacket::operator=(ScitForecastDataPacket&&) noexcept = default; - -const std::vector& ScitForecastDataPacket::data() const -{ - return p->data_; -} - -size_t ScitForecastDataPacket::RecordCount() const -{ - return p->recordCount_; -} - -bool ScitForecastDataPacket::ParseData(std::istream& is) -{ - bool blockValid = true; - - if (!packetCodes_.contains(packet_code())) - { - logger_->warn("Invalid packet code: {}", packet_code()); - blockValid = false; - } - - if (blockValid) - { - p->recordCount_ = length_of_block(); - p->data_.resize(p->recordCount_); - is.read(reinterpret_cast(p->data_.data()), p->recordCount_); - } - - return blockValid; -} - -std::shared_ptr -ScitForecastDataPacket::Create(std::istream& is) -{ - std::shared_ptr packet = - std::make_shared(); - - if (!packet->Parse(is)) - { - packet.reset(); - } - - return packet; -} - -} // namespace rpg -} // namespace wsr88d -} // namespace scwx diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 0652d898..30d1cd8c 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -152,7 +152,7 @@ set(HDR_WSR88D_RPG include/scwx/wsr88d/rpg/ccb_header.hpp include/scwx/wsr88d/rpg/radar_coded_message.hpp include/scwx/wsr88d/rpg/radial_data_packet.hpp include/scwx/wsr88d/rpg/raster_data_packet.hpp - include/scwx/wsr88d/rpg/scit_forecast_data_packet.hpp + include/scwx/wsr88d/rpg/scit_data_packet.hpp include/scwx/wsr88d/rpg/set_color_level_packet.hpp include/scwx/wsr88d/rpg/special_graphic_symbol_packet.hpp include/scwx/wsr88d/rpg/sti_circle_symbol_packet.hpp @@ -191,7 +191,7 @@ set(SRC_WSR88D_RPG source/scwx/wsr88d/rpg/ccb_header.cpp source/scwx/wsr88d/rpg/radar_coded_message.cpp source/scwx/wsr88d/rpg/radial_data_packet.cpp source/scwx/wsr88d/rpg/raster_data_packet.cpp - source/scwx/wsr88d/rpg/scit_forecast_data_packet.cpp + source/scwx/wsr88d/rpg/scit_data_packet.cpp source/scwx/wsr88d/rpg/set_color_level_packet.cpp source/scwx/wsr88d/rpg/special_graphic_symbol_packet.cpp source/scwx/wsr88d/rpg/sti_circle_symbol_packet.cpp From 43911c11795ede26d96f1ae230c71672cdec33cf Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 3 Feb 2024 23:43:08 -0600 Subject: [PATCH 02/37] Parse SCIT display data as packet list --- .../scwx/wsr88d/rpg/scit_data_packet.hpp | 2 +- .../scwx/wsr88d/rpg/scit_data_packet.cpp | 51 +++++++++++++++---- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/wxdata/include/scwx/wsr88d/rpg/scit_data_packet.hpp b/wxdata/include/scwx/wsr88d/rpg/scit_data_packet.hpp index 37aac08a..d051ed36 100644 --- a/wxdata/include/scwx/wsr88d/rpg/scit_data_packet.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/scit_data_packet.hpp @@ -24,7 +24,7 @@ class ScitDataPacket : public SpecialGraphicSymbolPacket ScitDataPacket(ScitDataPacket&&) noexcept; ScitDataPacket& operator=(ScitDataPacket&&) noexcept; - const std::vector& data() const; + std::vector> packet_list() const; std::size_t RecordCount() const override; diff --git a/wxdata/source/scwx/wsr88d/rpg/scit_data_packet.cpp b/wxdata/source/scwx/wsr88d/rpg/scit_data_packet.cpp index 74327c81..e63aa188 100644 --- a/wxdata/source/scwx/wsr88d/rpg/scit_data_packet.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/scit_data_packet.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -20,11 +21,10 @@ static const std::set packetCodes_ = {23, 24}; class ScitDataPacket::Impl { public: - explicit Impl() : data_ {}, recordCount_ {0} {} + explicit Impl() {} ~Impl() = default; - std::vector data_; - size_t recordCount_; + std::vector> packetList_ {}; }; ScitDataPacket::ScitDataPacket() : p(std::make_unique()) {} @@ -33,14 +33,14 @@ ScitDataPacket::~ScitDataPacket() = default; ScitDataPacket::ScitDataPacket(ScitDataPacket&&) noexcept = default; ScitDataPacket& ScitDataPacket::operator=(ScitDataPacket&&) noexcept = default; -const std::vector& ScitDataPacket::data() const +std::vector> ScitDataPacket::packet_list() const { - return p->data_; + return p->packetList_; } size_t ScitDataPacket::RecordCount() const { - return p->recordCount_; + return p->packetList_.size(); } bool ScitDataPacket::ParseData(std::istream& is) @@ -55,9 +55,42 @@ bool ScitDataPacket::ParseData(std::istream& is) if (blockValid) { - p->recordCount_ = length_of_block(); - p->data_.resize(p->recordCount_); - is.read(reinterpret_cast(p->data_.data()), p->recordCount_); + std::uint32_t bytesRead = 0; + std::uint32_t lengthOfBlock = length_of_block(); + std::streampos dataStart = is.tellg(); + std::streampos dataEnd = + dataStart + static_cast(lengthOfBlock); + + while (bytesRead < lengthOfBlock) + { + std::shared_ptr packet = PacketFactory::Create(is); + if (packet != nullptr) + { + p->packetList_.push_back(packet); + bytesRead += static_cast(packet->data_size()); + } + else + { + break; + } + } + + if (bytesRead < lengthOfBlock) + { + logger_->trace("Block bytes read smaller than size: {} < {} bytes", + bytesRead, + lengthOfBlock); + blockValid = false; + is.seekg(dataEnd, std::ios_base::beg); + } + if (bytesRead > lengthOfBlock) + { + logger_->warn("Block bytes read larger than size: {} > {} bytes", + bytesRead, + lengthOfBlock); + blockValid = false; + is.seekg(dataEnd, std::ios_base::beg); + } } return blockValid; From a5d97933dc4e971564a18abaaeaefe707faf125f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 3 Feb 2024 23:58:26 -0600 Subject: [PATCH 03/37] Add special symbol getter method to TextAndSpecialSymbolPacket --- wxdata/include/scwx/wsr88d/rpg/rpg_types.hpp | 22 +++++ .../rpg/text_and_special_symbol_packet.hpp | 21 ++--- .../rpg/text_and_special_symbol_packet.cpp | 80 +++++++++++++------ wxdata/wxdata.cmake | 1 + 4 files changed, 88 insertions(+), 36 deletions(-) create mode 100644 wxdata/include/scwx/wsr88d/rpg/rpg_types.hpp diff --git a/wxdata/include/scwx/wsr88d/rpg/rpg_types.hpp b/wxdata/include/scwx/wsr88d/rpg/rpg_types.hpp new file mode 100644 index 00000000..a278017c --- /dev/null +++ b/wxdata/include/scwx/wsr88d/rpg/rpg_types.hpp @@ -0,0 +1,22 @@ +#pragma once + +namespace scwx +{ +namespace wsr88d +{ +namespace rpg +{ + +enum class SpecialSymbol +{ + PastStormCellPosition, + CurrentStormCellPosition, + ForecastStormCellPosition, + PastMdaPosition, + ForecastMdaPosition, + None +}; + +} // namespace rpg +} // namespace wsr88d +} // namespace scwx diff --git a/wxdata/include/scwx/wsr88d/rpg/text_and_special_symbol_packet.hpp b/wxdata/include/scwx/wsr88d/rpg/text_and_special_symbol_packet.hpp index 18cccb75..b668002f 100644 --- a/wxdata/include/scwx/wsr88d/rpg/text_and_special_symbol_packet.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/text_and_special_symbol_packet.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -14,8 +15,6 @@ namespace wsr88d namespace rpg { -class TextAndSpecialSymbolPacketImpl; - class TextAndSpecialSymbolPacket : public Packet { public: @@ -29,21 +28,23 @@ class TextAndSpecialSymbolPacket : public Packet TextAndSpecialSymbolPacket(TextAndSpecialSymbolPacket&&) noexcept; TextAndSpecialSymbolPacket& operator=(TextAndSpecialSymbolPacket&&) noexcept; - uint16_t packet_code() const override; - uint16_t length_of_block() const; - std::optional value_of_text() const; - int16_t start_i() const; - int16_t start_j() const; - std::string text() const; + std::uint16_t packet_code() const override; + std::uint16_t length_of_block() const; + std::optional value_of_text() const; + std::int16_t start_i() const; + std::int16_t start_j() const; + std::string text() const; + SpecialSymbol special_symbol() const; - size_t data_size() const override; + std::size_t data_size() const override; bool Parse(std::istream& is) override; static std::shared_ptr Create(std::istream& is); private: - std::unique_ptr p; + class Impl; + std::unique_ptr p; }; } // namespace rpg diff --git a/wxdata/source/scwx/wsr88d/rpg/text_and_special_symbol_packet.cpp b/wxdata/source/scwx/wsr88d/rpg/text_and_special_symbol_packet.cpp index a0833717..313bc0e7 100644 --- a/wxdata/source/scwx/wsr88d/rpg/text_and_special_symbol_packet.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/text_and_special_symbol_packet.cpp @@ -15,53 +15,45 @@ static const std::string logPrefix_ = "scwx::wsr88d::rpg::text_and_special_symbol_packet"; static const auto logger_ = util::Logger::Create(logPrefix_); -class TextAndSpecialSymbolPacketImpl +class TextAndSpecialSymbolPacket::Impl { public: - explicit TextAndSpecialSymbolPacketImpl() : - packetCode_ {0}, - lengthOfBlock_ {0}, - valueOfText_ {0}, - startI_ {0}, - startJ_ {0}, - text_ {} - { - } - ~TextAndSpecialSymbolPacketImpl() = default; + explicit Impl() {} + ~Impl() = default; - uint16_t packetCode_; - uint16_t lengthOfBlock_; - uint16_t valueOfText_; - int16_t startI_; - int16_t startJ_; + std::uint16_t packetCode_ {0}; + std::uint16_t lengthOfBlock_ {0}; + std::uint16_t valueOfText_ {0}; + std::int16_t startI_ {0}; + std::int16_t startJ_ {0}; - std::string text_; + std::string text_ {}; }; TextAndSpecialSymbolPacket::TextAndSpecialSymbolPacket() : - p(std::make_unique()) + p(std::make_unique()) { } TextAndSpecialSymbolPacket::~TextAndSpecialSymbolPacket() = default; TextAndSpecialSymbolPacket::TextAndSpecialSymbolPacket( - TextAndSpecialSymbolPacket&&) noexcept = default; + TextAndSpecialSymbolPacket&&) noexcept = default; TextAndSpecialSymbolPacket& TextAndSpecialSymbolPacket::operator=( TextAndSpecialSymbolPacket&&) noexcept = default; -uint16_t TextAndSpecialSymbolPacket::packet_code() const +std::uint16_t TextAndSpecialSymbolPacket::packet_code() const { return p->packetCode_; } -uint16_t TextAndSpecialSymbolPacket::length_of_block() const +std::uint16_t TextAndSpecialSymbolPacket::length_of_block() const { return p->lengthOfBlock_; } -std::optional TextAndSpecialSymbolPacket::value_of_text() const +std::optional TextAndSpecialSymbolPacket::value_of_text() const { - std::optional value; + std::optional value; if (p->packetCode_ == 8) { @@ -71,12 +63,12 @@ std::optional TextAndSpecialSymbolPacket::value_of_text() const return value; } -int16_t TextAndSpecialSymbolPacket::start_i() const +std::int16_t TextAndSpecialSymbolPacket::start_i() const { return p->startI_; } -int16_t TextAndSpecialSymbolPacket::start_j() const +std::int16_t TextAndSpecialSymbolPacket::start_j() const { return p->startJ_; } @@ -86,7 +78,43 @@ std::string TextAndSpecialSymbolPacket::text() const return p->text_; } -size_t TextAndSpecialSymbolPacket::data_size() const +SpecialSymbol TextAndSpecialSymbolPacket::special_symbol() const +{ + SpecialSymbol symbol = SpecialSymbol::None; + + if (!p->text_.empty()) + { + switch (p->text_.at(0)) + { + case '!': // 0x21 + symbol = SpecialSymbol::PastStormCellPosition; + break; + + case '"': // 0x22 + symbol = SpecialSymbol::CurrentStormCellPosition; + break; + + case '#': // 0x23 + symbol = SpecialSymbol::ForecastStormCellPosition; + break; + + case '$': // 0x24 + symbol = SpecialSymbol::PastMdaPosition; + break; + + case '%': // 0x25 + symbol = SpecialSymbol::ForecastMdaPosition; + break; + + default: + break; + } + } + + return symbol; +} + +std::size_t TextAndSpecialSymbolPacket::data_size() const { return p->lengthOfBlock_ + 4u; } diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 30d1cd8c..8a885719 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -152,6 +152,7 @@ set(HDR_WSR88D_RPG include/scwx/wsr88d/rpg/ccb_header.hpp include/scwx/wsr88d/rpg/radar_coded_message.hpp include/scwx/wsr88d/rpg/radial_data_packet.hpp include/scwx/wsr88d/rpg/raster_data_packet.hpp + include/scwx/wsr88d/rpg/rpg_types.hpp include/scwx/wsr88d/rpg/scit_data_packet.hpp include/scwx/wsr88d/rpg/set_color_level_packet.hpp include/scwx/wsr88d/rpg/special_graphic_symbol_packet.hpp From b8398b4ad07c92dd01b994e2b601a0b4b650d624 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 4 Feb 2024 21:09:34 -0600 Subject: [PATCH 04/37] Add overlay product layer, to hold the following overlay products: - Storm Structure (NSS/62) - Hail Index (NHI/59) - Mesocyclone (NME/60, NMD/141) - Tornadic Vortex Signature (NTV/61) - Storm Tracking Information (NST/58) --- scwx-qt/scwx-qt.cmake | 2 + scwx-qt/source/scwx/qt/map/map_widget.cpp | 24 ++++-- .../scwx/qt/map/overlay_product_layer.cpp | 73 +++++++++++++++++++ .../scwx/qt/map/overlay_product_layer.hpp | 37 ++++++++++ scwx-qt/source/scwx/qt/model/layer_model.cpp | 1 + scwx-qt/source/scwx/qt/types/layer_types.cpp | 4 +- scwx-qt/source/scwx/qt/types/layer_types.hpp | 3 +- 7 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp create mode 100644 scwx-qt/source/scwx/qt/map/overlay_product_layer.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 09b1a8a3..9ecc6f07 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -115,6 +115,7 @@ set(HDR_MAP source/scwx/qt/map/alert_layer.hpp source/scwx/qt/map/map_settings.hpp source/scwx/qt/map/map_widget.hpp source/scwx/qt/map/overlay_layer.hpp + source/scwx/qt/map/overlay_product_layer.hpp source/scwx/qt/map/placefile_layer.hpp source/scwx/qt/map/radar_product_layer.hpp source/scwx/qt/map/radar_range_layer.hpp @@ -128,6 +129,7 @@ set(SRC_MAP source/scwx/qt/map/alert_layer.cpp source/scwx/qt/map/map_provider.cpp source/scwx/qt/map/map_widget.cpp source/scwx/qt/map/overlay_layer.cpp + source/scwx/qt/map/overlay_product_layer.cpp source/scwx/qt/map/placefile_layer.cpp source/scwx/qt/map/radar_product_layer.cpp source/scwx/qt/map/radar_range_layer.cpp diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index a5100e4a..1e65f262 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -181,12 +182,13 @@ class MapWidgetImpl : public QObject manager::PlacefileManager::Instance()}; std::shared_ptr radarProductManager_; - std::shared_ptr radarProductLayer_; - std::shared_ptr alertLayer_; - std::shared_ptr overlayLayer_; - std::shared_ptr placefileLayer_; - std::shared_ptr colorTableLayer_; - std::shared_ptr radarSiteLayer_ {nullptr}; + std::shared_ptr radarProductLayer_; + std::shared_ptr alertLayer_; + std::shared_ptr overlayLayer_; + std::shared_ptr overlayProductLayer_ {nullptr}; + std::shared_ptr placefileLayer_; + std::shared_ptr colorTableLayer_; + std::shared_ptr radarSiteLayer_ {nullptr}; std::list> placefileLayers_ {}; @@ -912,6 +914,16 @@ void MapWidgetImpl::AddLayer(types::LayerType type, { switch (std::get(description)) { + // If there is a radar product view, create the overlay product layer + case types::DataLayer::OverlayProduct: + if (radarProductView != nullptr) + { + overlayProductLayer_ = + std::make_shared(context_); + AddLayer(layerName, overlayProductLayer_, before); + } + break; + // If there is a radar product view, create the radar range layer case types::DataLayer::RadarRange: if (radarProductView != nullptr) diff --git a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp new file mode 100644 index 00000000..90e85372 --- /dev/null +++ b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp @@ -0,0 +1,73 @@ +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace map +{ + +static const std::string logPrefix_ = "scwx::qt::map::overlay_product_layer"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class OverlayProductLayer::Impl +{ +public: + explicit Impl(std::shared_ptr context) {} + ~Impl() = default; +}; + +OverlayProductLayer::OverlayProductLayer(std::shared_ptr context) : + DrawLayer(context), p(std::make_unique(context)) +{ +} + +OverlayProductLayer::~OverlayProductLayer() = default; + +void OverlayProductLayer::Initialize() +{ + logger_->debug("Initialize()"); + + DrawLayer::Initialize(); +} + +void OverlayProductLayer::Render( + const QMapLibreGL::CustomLayerRenderParameters& params) +{ + gl::OpenGLFunctions& gl = context()->gl(); + + // Set OpenGL blend mode for transparency + gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + DrawLayer::Render(params); + + SCWX_GL_CHECK_ERROR(); +} + +void OverlayProductLayer::Deinitialize() +{ + logger_->debug("Deinitialize()"); + + DrawLayer::Deinitialize(); +} + +bool OverlayProductLayer::RunMousePicking( + const QMapLibreGL::CustomLayerRenderParameters& params, + const QPointF& mouseLocalPos, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords, + const common::Coordinate& mouseGeoCoords, + std::shared_ptr& eventHandler) +{ + return DrawLayer::RunMousePicking(params, + mouseLocalPos, + mouseGlobalPos, + mouseCoords, + mouseGeoCoords, + eventHandler); +} + +} // namespace map +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/map/overlay_product_layer.hpp b/scwx-qt/source/scwx/qt/map/overlay_product_layer.hpp new file mode 100644 index 00000000..0b550697 --- /dev/null +++ b/scwx-qt/source/scwx/qt/map/overlay_product_layer.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace scwx +{ +namespace qt +{ +namespace map +{ + +class OverlayProductLayer : public DrawLayer +{ +public: + explicit OverlayProductLayer(std::shared_ptr context); + ~OverlayProductLayer(); + + void Initialize() override final; + void Render(const QMapLibreGL::CustomLayerRenderParameters&) override final; + void Deinitialize() override final; + + bool RunMousePicking( + const QMapLibreGL::CustomLayerRenderParameters& params, + const QPointF& mouseLocalPos, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords, + const common::Coordinate& mouseGeoCoords, + std::shared_ptr& eventHandler) override final; + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace map +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/model/layer_model.cpp b/scwx-qt/source/scwx/qt/model/layer_model.cpp index 9678b06b..894b7281 100644 --- a/scwx-qt/source/scwx/qt/model/layer_model.cpp +++ b/scwx-qt/source/scwx/qt/model/layer_model.cpp @@ -43,6 +43,7 @@ static const std::vector kDefaultLayers_ { types::InformationLayer::RadarSite, false, {false, false, false, false}}, + {types::LayerType::Data, types::DataLayer::OverlayProduct, true}, {types::LayerType::Data, types::DataLayer::RadarRange, true}, {types::LayerType::Alert, awips::Phenomenon::Tornado, true}, {types::LayerType::Alert, awips::Phenomenon::SnowSquall, true}, diff --git a/scwx-qt/source/scwx/qt/types/layer_types.cpp b/scwx-qt/source/scwx/qt/types/layer_types.cpp index d935cd42..6e66c5d1 100644 --- a/scwx-qt/source/scwx/qt/types/layer_types.cpp +++ b/scwx-qt/source/scwx/qt/types/layer_types.cpp @@ -23,7 +23,9 @@ static const std::unordered_map layerTypeName_ { {LayerType::Unknown, "?"}}; static const std::unordered_map dataLayerName_ { - {DataLayer::RadarRange, "Radar Range"}, {DataLayer::Unknown, "?"}}; + {DataLayer::OverlayProduct, "Overlay Product"}, + {DataLayer::RadarRange, "Radar Range"}, + {DataLayer::Unknown, "?"}}; static const std::unordered_map informationLayerName_ {{InformationLayer::MapOverlay, "Map Overlay"}, diff --git a/scwx-qt/source/scwx/qt/types/layer_types.hpp b/scwx-qt/source/scwx/qt/types/layer_types.hpp index 1309b648..f0561a6e 100644 --- a/scwx-qt/source/scwx/qt/types/layer_types.hpp +++ b/scwx-qt/source/scwx/qt/types/layer_types.hpp @@ -31,11 +31,12 @@ enum class LayerType enum class DataLayer { + OverlayProduct, RadarRange, Unknown }; typedef scwx::util:: - Iterator + Iterator DataLayerIterator; enum class InformationLayer From 949932313337c70ee644f31044e8f0e956a07e97 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 4 Feb 2024 21:17:56 -0600 Subject: [PATCH 05/37] Refactor GeoLine to GeoLines, in order to support multiple segments per draw item --- scwx-qt/scwx-qt.cmake | 4 +-- .../gl/draw/{geo_line.cpp => geo_lines.cpp} | 36 +++++++++---------- .../gl/draw/{geo_line.hpp => geo_lines.hpp} | 14 ++++---- 3 files changed, 27 insertions(+), 27 deletions(-) rename scwx-qt/source/scwx/qt/gl/draw/{geo_line.cpp => geo_lines.cpp} (91%) rename scwx-qt/source/scwx/qt/gl/draw/{geo_line.hpp => geo_lines.hpp} (83%) diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 9ecc6f07..a90645e9 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -61,7 +61,7 @@ set(SRC_GL source/scwx/qt/gl/gl_context.cpp source/scwx/qt/gl/shader_program.cpp) set(HDR_GL_DRAW source/scwx/qt/gl/draw/draw_item.hpp source/scwx/qt/gl/draw/geo_icons.hpp - source/scwx/qt/gl/draw/geo_line.hpp + source/scwx/qt/gl/draw/geo_lines.hpp source/scwx/qt/gl/draw/icons.hpp source/scwx/qt/gl/draw/placefile_icons.hpp source/scwx/qt/gl/draw/placefile_images.hpp @@ -72,7 +72,7 @@ set(HDR_GL_DRAW source/scwx/qt/gl/draw/draw_item.hpp source/scwx/qt/gl/draw/rectangle.hpp) set(SRC_GL_DRAW source/scwx/qt/gl/draw/draw_item.cpp source/scwx/qt/gl/draw/geo_icons.cpp - source/scwx/qt/gl/draw/geo_line.cpp + source/scwx/qt/gl/draw/geo_lines.cpp source/scwx/qt/gl/draw/icons.cpp source/scwx/qt/gl/draw/placefile_icons.cpp source/scwx/qt/gl/draw/placefile_images.cpp diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_line.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp similarity index 91% rename from scwx-qt/source/scwx/qt/gl/draw/geo_line.cpp rename to scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp index 17d84a79..7f05aaaf 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_line.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -16,7 +16,7 @@ namespace gl namespace draw { -static const std::string logPrefix_ = "scwx::qt::gl::draw::geo_line"; +static const std::string logPrefix_ = "scwx::qt::gl::draw::geo_lines"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static constexpr size_t kNumRectangles = 1; @@ -29,7 +29,7 @@ static constexpr size_t kBufferLength = static const std::string kTextureName = "lines/default-1x7"; -class GeoLine::Impl +class GeoLines::Impl { public: explicit Impl(std::shared_ptr context) : @@ -79,16 +79,16 @@ class GeoLine::Impl void Update(); }; -GeoLine::GeoLine(std::shared_ptr context) : +GeoLines::GeoLines(std::shared_ptr context) : DrawItem(context->gl()), p(std::make_unique(context)) { } -GeoLine::~GeoLine() = default; +GeoLines::~GeoLines() = default; -GeoLine::GeoLine(GeoLine&&) noexcept = default; -GeoLine& GeoLine::operator=(GeoLine&&) noexcept = default; +GeoLines::GeoLines(GeoLines&&) noexcept = default; +GeoLines& GeoLines::operator=(GeoLines&&) noexcept = default; -void GeoLine::Initialize() +void GeoLines::Initialize() { gl::OpenGLFunctions& gl = p->context_->gl(); @@ -166,7 +166,7 @@ void GeoLine::Initialize() p->dirty_ = true; } -void GeoLine::Render(const QMapLibreGL::CustomLayerRenderParameters& params) +void GeoLines::Render(const QMapLibreGL::CustomLayerRenderParameters& params) { if (p->visible_) { @@ -186,7 +186,7 @@ void GeoLine::Render(const QMapLibreGL::CustomLayerRenderParameters& params) } } -void GeoLine::Deinitialize() +void GeoLines::Deinitialize() { gl::OpenGLFunctions& gl = p->context_->gl(); @@ -194,10 +194,10 @@ void GeoLine::Deinitialize() gl.glDeleteBuffers(1, &p->vbo_); } -void GeoLine::SetPoints(float latitude1, - float longitude1, - float latitude2, - float longitude2) +void GeoLines::SetPoints(float latitude1, + float longitude1, + float latitude2, + float longitude2) { if (p->points_[0].latitude_ != latitude1 || p->points_[0].longitude_ != longitude1 || @@ -221,7 +221,7 @@ void GeoLine::SetPoints(float latitude1, } } -void GeoLine::SetModulateColor(boost::gil::rgba8_pixel_t color) +void GeoLines::SetModulateColor(boost::gil::rgba8_pixel_t color) { if (p->modulateColor_ != color) { @@ -230,7 +230,7 @@ void GeoLine::SetModulateColor(boost::gil::rgba8_pixel_t color) } } -void GeoLine::SetWidth(float width) +void GeoLines::SetWidth(float width) { if (p->width_ != width) { @@ -239,12 +239,12 @@ void GeoLine::SetWidth(float width) } } -void GeoLine::SetVisible(bool visible) +void GeoLines::SetVisible(bool visible) { p->visible_ = visible; } -void GeoLine::Impl::Update() +void GeoLines::Impl::Update() { if (dirty_) { diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_line.hpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp similarity index 83% rename from scwx-qt/source/scwx/qt/gl/draw/geo_line.hpp rename to scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp index 03bd9df2..42b04447 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_line.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp @@ -14,17 +14,17 @@ namespace gl namespace draw { -class GeoLine : public DrawItem +class GeoLines : public DrawItem { public: - explicit GeoLine(std::shared_ptr context); - ~GeoLine(); + explicit GeoLines(std::shared_ptr context); + ~GeoLines(); - GeoLine(const GeoLine&) = delete; - GeoLine& operator=(const GeoLine&) = delete; + GeoLines(const GeoLines&) = delete; + GeoLines& operator=(const GeoLines&) = delete; - GeoLine(GeoLine&&) noexcept; - GeoLine& operator=(GeoLine&&) noexcept; + GeoLines(GeoLines&&) noexcept; + GeoLines& operator=(GeoLines&&) noexcept; void Initialize() override; void Render(const QMapLibreGL::CustomLayerRenderParameters& params) override; From 17368b2404866dba4bafd066456e3b03c38d4858 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 7 Feb 2024 05:35:48 -0600 Subject: [PATCH 06/37] Overhaul GeoLines to be more in line with GeoIcons and PlacefileLines --- scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp | 599 ++++++++++++++----- scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp | 103 +++- 2 files changed, 530 insertions(+), 172 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp index 7f05aaaf..bcf6214b 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp @@ -1,11 +1,12 @@ #include #include -#include -#include +#include +#include #include -#include -#include +#include + +#include namespace scwx { @@ -23,29 +24,53 @@ static constexpr size_t kNumRectangles = 1; static constexpr size_t kNumTriangles = kNumRectangles * 2; static constexpr size_t kVerticesPerTriangle = 3; static constexpr size_t kVerticesPerRectangle = kVerticesPerTriangle * 2; -static constexpr size_t kPointsPerVertex = 11; +static constexpr size_t kPointsPerVertex = 9; static constexpr size_t kBufferLength = kNumTriangles * kVerticesPerTriangle * kPointsPerVertex; -static const std::string kTextureName = "lines/default-1x7"; +// Threshold, start time, end time +static constexpr std::size_t kIntegersPerVertex_ = 3; + +struct GeoLineDrawItem +{ + bool visible_ {true}; + units::length::nautical_miles threshold_ {}; + std::chrono::sys_time startTime_ {}; + std::chrono::sys_time endTime_ {}; + + boost::gil::rgba32f_pixel_t modulate_ {1.0f, 1.0f, 1.0f, 1.0f}; + float latitude1_ {}; + float longitude1_ {}; + float latitude2_ {}; + float longitude2_ {}; + float width_ {5.0}; + units::angle::degrees angle_ {}; + std::string hoverText_ {}; +}; class GeoLines::Impl { public: + struct LineHoverEntry + { + std::shared_ptr di_; + + glm::vec2 p1_; + glm::vec2 p2_; + glm::vec2 otl_; + glm::vec2 otr_; + glm::vec2 obl_; + glm::vec2 obr_; + }; + explicit Impl(std::shared_ptr context) : context_ {context}, - geodesic_ {util::GeographicLib::DefaultGeodesic()}, - dirty_ {false}, - visible_ {true}, - points_ {}, - angle_ {}, - width_ {7.0f}, - modulateColor_ {std::nullopt}, shaderProgram_ {nullptr}, uMVPMatrixLocation_(GL_INVALID_INDEX), uMapMatrixLocation_(GL_INVALID_INDEX), uMapScreenCoordLocation_(GL_INVALID_INDEX), - texture_ {}, + uMapDistanceLocation_(GL_INVALID_INDEX), + uSelectedTimeLocation_(GL_INVALID_INDEX), vao_ {GL_INVALID_INDEX}, vbo_ {GL_INVALID_INDEX} { @@ -53,30 +78,40 @@ class GeoLines::Impl ~Impl() {} + void BufferLine(const std::shared_ptr& di); + void Update(); + void UpdateBuffers(); + std::shared_ptr context_; - const GeographicLib::Geodesic& geodesic_; + bool visible_ {true}; + bool dirty_ {false}; + bool thresholded_ {false}; + + std::chrono::system_clock::time_point selectedTime_ {}; - bool dirty_; + std::mutex lineMutex_ {}; - bool visible_; - std::array points_; - float angle_; - float width_; + std::vector> currentLineList_ {}; + std::vector> newLineList_ {}; - std::optional modulateColor_; + std::vector currentLinesBuffer_ {}; + std::vector currentIntegerBuffer_ {}; + std::vector newLinesBuffer_ {}; + std::vector newIntegerBuffer_ {}; + + std::vector currentHoverLines_ {}; + std::vector newHoverLines_ {}; std::shared_ptr shaderProgram_; GLint uMVPMatrixLocation_; GLint uMapMatrixLocation_; GLint uMapScreenCoordLocation_; + GLint uMapDistanceLocation_; + GLint uSelectedTimeLocation_; - util::TextureAttributes texture_; - - GLuint vao_; - GLuint vbo_; - - void Update(); + GLuint vao_; + std::array vbo_; }; GeoLines::GeoLines(std::shared_ptr context) : @@ -93,37 +128,24 @@ void GeoLines::Initialize() gl::OpenGLFunctions& gl = p->context_->gl(); p->shaderProgram_ = p->context_->GetShaderProgram( - ":/gl/geo_line.vert", ":/gl/texture2d_array.frag"); - - p->uMVPMatrixLocation_ = - gl.glGetUniformLocation(p->shaderProgram_->id(), "uMVPMatrix"); - if (p->uMVPMatrixLocation_ == -1) - { - logger_->warn("Could not find uMVPMatrix"); - } - - p->uMapMatrixLocation_ = - gl.glGetUniformLocation(p->shaderProgram_->id(), "uMapMatrix"); - if (p->uMapMatrixLocation_ == -1) - { - logger_->warn("Could not find uMapMatrix"); - } + {{GL_VERTEX_SHADER, ":/gl/geo_texture2d.vert"}, + {GL_GEOMETRY_SHADER, ":/gl/threshold.geom"}, + {GL_FRAGMENT_SHADER, ":/gl/color.frag"}}); + p->uMVPMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMVPMatrix"); + p->uMapMatrixLocation_ = p->shaderProgram_->GetUniformLocation("uMapMatrix"); p->uMapScreenCoordLocation_ = - gl.glGetUniformLocation(p->shaderProgram_->id(), "uMapScreenCoord"); - if (p->uMapScreenCoordLocation_ == -1) - { - logger_->warn("Could not find uMapScreenCoord"); - } - - p->texture_ = - util::TextureAtlas::Instance().GetTextureAttributes(kTextureName); + p->shaderProgram_->GetUniformLocation("uMapScreenCoord"); + p->uMapDistanceLocation_ = + p->shaderProgram_->GetUniformLocation("uMapDistance"); + p->uSelectedTimeLocation_ = + p->shaderProgram_->GetUniformLocation("uSelectedTime"); gl.glGenVertexArrays(1, &p->vao_); - gl.glGenBuffers(1, &p->vbo_); + gl.glGenBuffers(static_cast(p->vbo_.size()), p->vbo_.data()); gl.glBindVertexArray(p->vao_); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_); + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[0]); gl.glBufferData( GL_ARRAY_BUFFER, sizeof(float) * kBufferLength, nullptr, GL_DYNAMIC_DRAW); @@ -145,44 +167,96 @@ void GeoLines::Initialize() reinterpret_cast(2 * sizeof(float))); gl.glEnableVertexAttribArray(1); - // aTexCoord - gl.glVertexAttribPointer(2, - 3, + // aModulate + gl.glVertexAttribPointer(3, + 4, GL_FLOAT, GL_FALSE, kPointsPerVertex * sizeof(float), reinterpret_cast(4 * sizeof(float))); - gl.glEnableVertexAttribArray(2); + gl.glEnableVertexAttribArray(3); - // aModulate - gl.glVertexAttribPointer(3, - 4, + // aAngle + gl.glVertexAttribPointer(4, + 1, GL_FLOAT, GL_FALSE, kPointsPerVertex * sizeof(float), - reinterpret_cast(7 * sizeof(float))); - gl.glEnableVertexAttribArray(3); + reinterpret_cast(8 * sizeof(float))); + gl.glEnableVertexAttribArray(4); + + gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_[1]); + gl.glBufferData(GL_ARRAY_BUFFER, 0u, nullptr, GL_DYNAMIC_DRAW); + + // aThreshold + gl.glVertexAttribIPointer(5, // + 1, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + static_cast(0)); + gl.glEnableVertexAttribArray(5); + + // aTimeRange + gl.glVertexAttribIPointer(6, // + 2, + GL_INT, + kIntegersPerVertex_ * sizeof(GLint), + reinterpret_cast(1 * sizeof(GLint))); + gl.glEnableVertexAttribArray(6); p->dirty_ = true; } void GeoLines::Render(const QMapLibreGL::CustomLayerRenderParameters& params) { - if (p->visible_) + if (!p->visible_) + { + return; + } + + std::unique_lock lock {p->lineMutex_}; + + if (p->currentLineList_.size() > 0) { gl::OpenGLFunctions& gl = p->context_->gl(); gl.glBindVertexArray(p->vao_); - gl.glBindBuffer(GL_ARRAY_BUFFER, p->vbo_); p->Update(); p->shaderProgram_->Use(); - UseDefaultProjection(params, p->uMVPMatrixLocation_); + UseRotationProjection(params, p->uMVPMatrixLocation_); UseMapProjection( params, p->uMapMatrixLocation_, p->uMapScreenCoordLocation_); - // Draw line - gl.glDrawArrays(GL_TRIANGLES, 0, 6); + if (p->thresholded_) + { + // If thresholding is enabled, set the map distance + units::length::nautical_miles mapDistance = + util::maplibre::GetMapDistance(params); + gl.glUniform1f(p->uMapDistanceLocation_, mapDistance.value()); + } + else + { + // If thresholding is disabled, set the map distance to 0 + gl.glUniform1f(p->uMapDistanceLocation_, 0.0f); + } + + // Selected time + std::chrono::system_clock::time_point selectedTime = + (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? + std::chrono::system_clock::now() : + p->selectedTime_; + gl.glUniform1i( + p->uSelectedTimeLocation_, + static_cast(std::chrono::duration_cast( + selectedTime.time_since_epoch()) + .count())); + + // Draw icons + gl.glDrawArrays(GL_TRIANGLES, + 0, + static_cast(p->currentLineList_.size() * + kVerticesPerRectangle)); } } @@ -191,121 +265,342 @@ void GeoLines::Deinitialize() gl::OpenGLFunctions& gl = p->context_->gl(); gl.glDeleteVertexArrays(1, &p->vao_); - gl.glDeleteBuffers(1, &p->vbo_); + gl.glDeleteBuffers(2, p->vbo_.data()); + + std::unique_lock lock {p->lineMutex_}; + + p->currentLinesBuffer_.clear(); + p->currentIntegerBuffer_.clear(); + p->currentHoverLines_.clear(); } -void GeoLines::SetPoints(float latitude1, - float longitude1, - float latitude2, - float longitude2) +void GeoLines::SetVisible(bool visible) { - if (p->points_[0].latitude_ != latitude1 || - p->points_[0].longitude_ != longitude1 || - p->points_[1].latitude_ != latitude2 || - p->points_[1].longitude_ != longitude2) - { - p->points_[0] = {latitude1, longitude1}; - p->points_[1] = {latitude2, longitude2}; - - double azi1; // Azimuth at point 1 (degrees) - double azi2; // (Forward) azimuth at point 2 (degrees) - p->geodesic_.Inverse(p->points_[0].latitude_, - p->points_[0].longitude_, - p->points_[1].latitude_, - p->points_[1].longitude_, - azi1, - azi2); - p->angle_ = -azi1 * std::numbers::pi / 180.0; - - p->dirty_ = true; - } + p->visible_ = visible; } -void GeoLines::SetModulateColor(boost::gil::rgba8_pixel_t color) +void GeoLines::StartLines() { - if (p->modulateColor_ != color) - { - p->modulateColor_ = color; - p->dirty_ = true; - } + // Clear the new buffers + p->newLineList_.clear(); + p->newLinesBuffer_.clear(); + p->newIntegerBuffer_.clear(); + p->newHoverLines_.clear(); } -void GeoLines::SetWidth(float width) +std::shared_ptr GeoLines::AddLine() { - if (p->width_ != width) - { - p->width_ = width; - p->dirty_ = true; - } + return p->newLineList_.emplace_back(std::make_shared()); } -void GeoLines::SetVisible(bool visible) +void GeoLines::SetLineLocation(const std::shared_ptr& di, + float latitude1, + float longitude1, + float latitude2, + float longitude2) { - p->visible_ = visible; + di->latitude1_ = latitude1; + di->longitude1_ = longitude1; + di->latitude2_ = latitude2; + di->longitude2_ = longitude2; } -void GeoLines::Impl::Update() +void GeoLines::SetLineModulate(const std::shared_ptr& di, + boost::gil::rgba8_pixel_t modulate) { - if (dirty_) - { - gl::OpenGLFunctions& gl = context_->gl(); + di->modulate_ = {modulate[0] / 255.0f, + modulate[1] / 255.0f, + modulate[2] / 255.0f, + modulate[3] / 255.0f}; +} - texture_ = - util::TextureAtlas::Instance().GetTextureAttributes(kTextureName); +void GeoLines::SetLineModulate(const std::shared_ptr& di, + boost::gil::rgba32f_pixel_t modulate) +{ + di->modulate_ = modulate; +} - // Latitude and longitude coordinates in degrees - const float lx = points_[0].latitude_; - const float rx = points_[1].latitude_; - const float by = points_[0].longitude_; - const float ty = points_[1].longitude_; +void GeoLines::SetLineWidth(const std::shared_ptr& di, + float width) +{ + di->width_ = width; +} - // Offset x/y in pixels - const float ox = width_ * 0.5f * cosf(angle_); - const float oy = width_ * 0.5f * sinf(angle_); +void GeoLines::SetLineVisible(const std::shared_ptr& di, + bool visible) +{ + di->visible_ = visible; +} - // Texture coordinates - static constexpr float r = 0.0f; +void GeoLines::SetLineHoverText(const std::shared_ptr& di, + const std::string& text) +{ + di->hoverText_ = text; +} - const float ls = texture_.sLeft_; - const float rs = texture_.sRight_; - const float tt = texture_.tTop_; - const float bt = texture_.tBottom_; +void GeoLines::FinishLines() +{ + // Update buffers + p->UpdateBuffers(); - float mc0 = 1.0f; - float mc1 = 1.0f; - float mc2 = 1.0f; - float mc3 = 1.0f; + std::unique_lock lock {p->lineMutex_}; - if (modulateColor_.has_value()) - { - boost::gil::rgba8_pixel_t& mc = modulateColor_.value(); + // Swap buffers + p->currentLineList_.swap(p->newLineList_); + p->currentLinesBuffer_.swap(p->newLinesBuffer_); + p->currentIntegerBuffer_.swap(p->newIntegerBuffer_); + p->currentHoverLines_.swap(p->newHoverLines_); - mc0 = mc[0] / 255.0f; - mc1 = mc[1] / 255.0f; - mc2 = mc[2] / 255.0f; - mc3 = mc[3] / 255.0f; - } + // Clear the new buffers, except the full line list (used to update buffers + // without re-adding lines) + p->newLinesBuffer_.clear(); + p->newIntegerBuffer_.clear(); + p->newHoverLines_.clear(); + + // Mark the draw item dirty + p->dirty_ = true; +} + +void GeoLines::Impl::UpdateBuffers() +{ + newLinesBuffer_.clear(); + newLinesBuffer_.reserve(newLineList_.size() * kBufferLength); + newIntegerBuffer_.clear(); + newIntegerBuffer_.reserve(newLineList_.size() * kVerticesPerRectangle * + kIntegersPerVertex_); + newHoverLines_.clear(); + + for (auto& di : newLineList_) + { + BufferLine(di); + } +} - const float buffer[kNumRectangles][kVerticesPerRectangle] - [kPointsPerVertex] = // - { // - // Line - { - {lx, by, -ox, -oy, ls, bt, r, mc0, mc1, mc2, mc3}, // BL - {lx, by, +ox, +oy, ls, tt, r, mc0, mc1, mc2, mc3}, // TL - {rx, ty, -ox, -oy, rs, bt, r, mc0, mc1, mc2, mc3}, // BR - {rx, ty, -ox, -oy, rs, bt, r, mc0, mc1, mc2, mc3}, // BR - {rx, ty, +ox, +oy, rs, tt, r, mc0, mc1, mc2, mc3}, // TR - {lx, by, +ox, +oy, ls, tt, r, mc0, mc1, mc2, mc3} // TL - }}; +void GeoLines::Impl::BufferLine( + const std::shared_ptr& di) +{ + // Threshold value + units::length::nautical_miles threshold = di->threshold_; + GLint thresholdValue = static_cast(std::round(threshold.value())); + + // Start and end time + GLint startTime = + static_cast(std::chrono::duration_cast( + di->startTime_.time_since_epoch()) + .count()); + GLint endTime = + static_cast(std::chrono::duration_cast( + di->endTime_.time_since_epoch()) + .count()); + + // Latitude and longitude coordinates in degrees + const float lat1 = di->latitude1_; + const float lon1 = di->longitude1_; + const float lat2 = di->latitude2_; + const float lon2 = di->longitude2_; + + // TODO: Base X/Y offsets in pixels + // const float x1 = static_cast(di->x1_); + // const float y1 = static_cast(di->y1_); + // const float x2 = static_cast(di->x2_); + // const float y2 = static_cast(di->y2_); + + // Angle + const units::angle::degrees angle = + util::GeographicLib::GetAngle(lat1, lon1, lat2, lon2); + const float a = static_cast(angle.value()); + + // Final X/Y offsets in pixels + const float hw = di->width_ * 0.5f; + const float lx = -hw; + const float rx = +hw; + const float ty = +hw; + const float by = -hw; + + // Modulate color + const float mc0 = di->modulate_[0]; + const float mc1 = di->modulate_[1]; + const float mc2 = di->modulate_[2]; + const float mc3 = di->modulate_[3]; + + // Update buffers + newLinesBuffer_.insert(newLinesBuffer_.end(), + { + // Line + lat1, lon1, lx, by, mc0, mc1, mc2, mc3, a, // BL + lat2, lon2, lx, ty, mc0, mc1, mc2, mc3, a, // TL + lat1, lon1, rx, by, mc0, mc1, mc2, mc3, a, // BR + lat1, lon1, rx, by, mc0, mc1, mc2, mc3, a, // BR + lat2, lon2, rx, ty, mc0, mc1, mc2, mc3, a, // TR + lat2, lon2, lx, ty, mc0, mc1, mc2, mc3, a // TL + }); + newIntegerBuffer_.insert(newIntegerBuffer_.end(), + {thresholdValue, + startTime, + endTime, + thresholdValue, + startTime, + endTime, + thresholdValue, + startTime, + endTime, + thresholdValue, + startTime, + endTime, + thresholdValue, + startTime, + endTime, + thresholdValue, + startTime, + endTime}); + + if (!di->hoverText_.empty()) + { + const units::angle::radians radians = angle; + + const auto sc1 = util::maplibre::LatLongToScreenCoordinate({lat1, lon1}); + const auto sc2 = util::maplibre::LatLongToScreenCoordinate({lat2, lon2}); + + const float cosAngle = cosf(static_cast(radians.value())); + const float sinAngle = sinf(static_cast(radians.value())); + const glm::mat2 rotate {cosAngle, -sinAngle, sinAngle, cosAngle}; + + const glm::vec2 otl = rotate * glm::vec2 {-hw, +hw}; + const glm::vec2 otr = rotate * glm::vec2 {+hw, +hw}; + const glm::vec2 obl = rotate * glm::vec2 {-hw, -hw}; + const glm::vec2 obr = rotate * glm::vec2 {+hw, -hw}; + + newHoverLines_.emplace_back( + LineHoverEntry {di, sc1, sc2, otl, otr, obl, obr}); + } +} + +void GeoLines::Impl::Update() +{ + // If the lines have been updated + if (dirty_) + { + gl::OpenGLFunctions& gl = context_->gl(); + + // Buffer lines data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[0]); + gl.glBufferData(GL_ARRAY_BUFFER, + sizeof(float) * currentLinesBuffer_.size(), + currentLinesBuffer_.data(), + GL_DYNAMIC_DRAW); + + // Buffer threshold data + gl.glBindBuffer(GL_ARRAY_BUFFER, vbo_[1]); gl.glBufferData(GL_ARRAY_BUFFER, - sizeof(float) * kBufferLength, - buffer, + sizeof(GLint) * currentIntegerBuffer_.size(), + currentIntegerBuffer_.data(), GL_DYNAMIC_DRAW); + } - dirty_ = false; + dirty_ = false; +} + +bool GeoLines::RunMousePicking( + const QMapLibreGL::CustomLayerRenderParameters& params, + const QPointF& /* mouseLocalPos */, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords, + const common::Coordinate& /* mouseGeoCoords */, + std::shared_ptr& /* eventHandler */) +{ + std::unique_lock lock {p->lineMutex_}; + + bool itemPicked = false; + + // Calculate map scale, remove width and height from original calculation + glm::vec2 scale = util::maplibre::GetMapScale(params); + scale = 2.0f / glm::vec2 {scale.x * params.width, scale.y * params.height}; + + // Scale and rotate the identity matrix to create the map matrix + glm::mat4 mapMatrix {1.0f}; + mapMatrix = glm::scale(mapMatrix, glm::vec3 {scale, 1.0f}); + mapMatrix = glm::rotate(mapMatrix, + glm::radians(params.bearing), + glm::vec3(0.0f, 0.0f, 1.0f)); + + units::length::meters mapDistance = + (p->thresholded_) ? util::maplibre::GetMapDistance(params) : + units::length::meters {0.0}; + + // If no time has been selected, use the current time + std::chrono::system_clock::time_point selectedTime = + (p->selectedTime_ == std::chrono::system_clock::time_point {}) ? + std::chrono::system_clock::now() : + p->selectedTime_; + + // For each pickable line + auto it = std::find_if( + std::execution::par_unseq, + p->currentHoverLines_.crbegin(), + p->currentHoverLines_.crend(), + [&mapDistance, &selectedTime, &mapMatrix, &mouseCoords](const auto& line) + { + if (( + // Placefile is thresholded + mapDistance > units::length::meters {0.0} && + + // Placefile threshold is < 999 nmi + static_cast(std::round( + units::length::nautical_miles {line.di_->threshold_} + .value())) < 999 && + + // Map distance is beyond the threshold + line.di_->threshold_ < mapDistance) || + + ( + // Line has a start time + line.di_->startTime_ != + std::chrono::system_clock::time_point {} && + + // The time range has not yet started + (selectedTime < line.di_->startTime_ || + + // The time range has ended + line.di_->endTime_ <= selectedTime))) + { + // Line is not pickable + return false; + } + + // Initialize vertices + glm::vec2 bl = line.p1_; + glm::vec2 br = bl; + glm::vec2 tl = line.p2_; + glm::vec2 tr = tl; + + // Calculate offsets + // - Rotated offset is half the line width (pixels) in each direction + // - Multiply the offset by the scaled and rotated map matrix + const glm::vec2 otl = mapMatrix * glm::vec4 {line.otl_, 0.0f, 1.0f}; + const glm::vec2 obl = mapMatrix * glm::vec4 {line.obl_, 0.0f, 1.0f}; + const glm::vec2 obr = mapMatrix * glm::vec4 {line.obr_, 0.0f, 1.0f}; + const glm::vec2 otr = mapMatrix * glm::vec4 {line.otr_, 0.0f, 1.0f}; + + // Offset vertices + tl += otl; + bl += obl; + br += obr; + tr += otr; + + // TODO: X/Y offsets + + // Test point against polygon bounds + return util::maplibre::IsPointInPolygon({tl, bl, br, tr}, mouseCoords); + }); + + if (it != p->currentHoverLines_.crend()) + { + itemPicked = true; + util::tooltip::Show(it->di_->hoverText_, mouseGlobalPos); } + + return itemPicked; } } // namespace draw diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp index 42b04447..e7564698 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.hpp @@ -14,6 +14,8 @@ namespace gl namespace draw { +struct GeoLineDrawItem; + class GeoLines : public DrawItem { public: @@ -26,48 +28,109 @@ class GeoLines : public DrawItem GeoLines(GeoLines&&) noexcept; GeoLines& operator=(GeoLines&&) noexcept; + void set_selected_time(std::chrono::system_clock::time_point selectedTime); + void set_thresholded(bool thresholded); + void Initialize() override; void Render(const QMapLibreGL::CustomLayerRenderParameters& params) override; void Deinitialize() override; + bool + RunMousePicking(const QMapLibreGL::CustomLayerRenderParameters& params, + const QPointF& mouseLocalPos, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords, + const common::Coordinate& mouseGeoCoords, + std::shared_ptr& eventHandler) override; + + /** + * Sets the visibility of the geo lines. + * + * @param [in] visible Line visibility + */ + void SetVisible(bool visible); + + /** + * Resets and prepares the draw item for adding a new set of lines. + */ + void StartLines(); + /** - * Sets the geographic coordinate endpoints associated with the line. + * Adds a geo line to the internal draw list. * - * @param latitude1 Latitude of the first endpoint in degrees - * @param longitude1 Longitude of the first endpoint in degrees - * @param latitude2 Latitude of the second endpoint in degrees - * @param longitude2 Longitude of the second endpoint in degrees + * @return Geo line draw item */ - void SetPoints(float latitude1, - float longitude1, - float latitude2, - float longitude2); + std::shared_ptr AddLine(); /** - * Sets the modulate color of the line. If specified, the texture color will - * be multiplied by the modulate color to produce the result. + * Sets the location of a geo line. * - * @param color Modulate color (RGBA) + * @param [in] di Geo line draw item + * @param [in] latitude1 The latitude of the first endpoint of the geo line + * in degrees. + * @param [in] longitude1 The longitude of the first endpoint of the geo line + * in degrees. + * @param [in] latitude2 The latitude of the second endpoint of the geo line + * in degrees. + * @param [in] longitude2 The longitude of the second endpoint of the geo + * line in degrees. */ - void SetModulateColor(boost::gil::rgba8_pixel_t color); + static void SetLineLocation(const std::shared_ptr& di, + float latitude1, + float longitude1, + float latitude2, + float longitude2); /** - * Sets the width of the line. + * Sets the modulate color of a geo line. * - * @param width Width in pixels + * @param [in] di Geo line draw item + * @param [in] modulate Modulate color */ - void SetWidth(float width); + static void SetLineModulate(const std::shared_ptr& di, + boost::gil::rgba8_pixel_t color); /** - * Sets the visibility of the line. + * Sets the modulate color of a geo line. * - * @param visible + * @param [in] di Geo line draw item + * @param [in] modulate Modulate color */ - void SetVisible(bool visible); + static void SetLineModulate(const std::shared_ptr& di, + boost::gil::rgba32f_pixel_t modulate); + + /** + * Sets the width of the geo line. + * + * @param [in] width Width in pixels + */ + static void SetLineWidth(const std::shared_ptr& di, + float width); + + /** + * Sets the visibility of the geo line. + * + * @param [in] visible + */ + static void SetLineVisible(const std::shared_ptr& di, + bool visible); + + /** + * Sets the hover text of a geo line. + * + * @param [in] di Geo line draw item + * @param [in] text Hover text + */ + static void SetLineHoverText(const std::shared_ptr& di, + const std::string& text); + + /** + * Finalizes the draw item after adding new lines. + */ + void FinishLines(); private: class Impl; - std::unique_ptr p; }; From 5d828b08197a00df661cbc5fe1019a717f1f4d33 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 10 Feb 2024 00:07:34 -0600 Subject: [PATCH 07/37] Expose i and j from LinkedVectorPacket --- .../scwx/wsr88d/rpg/linked_vector_packet.hpp | 22 ++++++-- .../scwx/wsr88d/rpg/linked_vector_packet.cpp | 50 +++++++++++++++++++ 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/wxdata/include/scwx/wsr88d/rpg/linked_vector_packet.hpp b/wxdata/include/scwx/wsr88d/rpg/linked_vector_packet.hpp index 47c03070..97b47696 100644 --- a/wxdata/include/scwx/wsr88d/rpg/linked_vector_packet.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/linked_vector_packet.hpp @@ -6,6 +6,8 @@ #include #include +#include + namespace scwx { namespace wsr88d @@ -21,17 +23,27 @@ class LinkedVectorPacket : public Packet explicit LinkedVectorPacket(); ~LinkedVectorPacket(); - LinkedVectorPacket(const LinkedVectorPacket&) = delete; + LinkedVectorPacket(const LinkedVectorPacket&) = delete; LinkedVectorPacket& operator=(const LinkedVectorPacket&) = delete; LinkedVectorPacket(LinkedVectorPacket&&) noexcept; LinkedVectorPacket& operator=(LinkedVectorPacket&&) noexcept; - uint16_t packet_code() const override; - uint16_t length_of_block() const; - std::optional value_of_vector() const; + std::uint16_t packet_code() const override; + std::uint16_t length_of_block() const; + std::optional value_of_vector() const; + + std::int16_t start_i() const; + std::int16_t start_j() const; + std::vector end_i() const; + std::vector end_j() const; + + units::kilometers start_i_km() const; + units::kilometers start_j_km() const; + std::vector> end_i_km() const; + std::vector> end_j_km() const; - size_t data_size() const override; + std::size_t data_size() const override; bool Parse(std::istream& is) override; diff --git a/wxdata/source/scwx/wsr88d/rpg/linked_vector_packet.cpp b/wxdata/source/scwx/wsr88d/rpg/linked_vector_packet.cpp index f12c35dd..ba9ba522 100644 --- a/wxdata/source/scwx/wsr88d/rpg/linked_vector_packet.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/linked_vector_packet.cpp @@ -71,6 +71,56 @@ std::optional LinkedVectorPacket::value_of_vector() const return value; } +std::int16_t LinkedVectorPacket::start_i() const +{ + return p->startI_; +} + +std::int16_t LinkedVectorPacket::start_j() const +{ + return p->startJ_; +} + +std::vector LinkedVectorPacket::end_i() const +{ + return p->endI_; +} + +std::vector LinkedVectorPacket::end_j() const +{ + return p->endJ_; +} + +units::kilometers LinkedVectorPacket::start_i_km() const +{ + return units::kilometers {p->startI_ * 0.25}; +} + +units::kilometers LinkedVectorPacket::start_j_km() const +{ + return units::kilometers {p->startJ_ * 0.25}; +} + +std::vector> LinkedVectorPacket::end_i_km() const +{ + std::vector> endI; + for (const auto& i : p->endI_) + { + endI.emplace_back(units::kilometers {i * 0.25}); + } + return endI; +} + +std::vector> LinkedVectorPacket::end_j_km() const +{ + std::vector> endJ; + for (const auto& j : p->endJ_) + { + endJ.emplace_back(units::kilometers {j * 0.25}); + } + return endJ; +} + size_t LinkedVectorPacket::data_size() const { return p->lengthOfBlock_ + 4u; From 8ae973c4cb4ebf4fd27ed65cbc3831098b790d2e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 10 Feb 2024 00:08:13 -0600 Subject: [PATCH 08/37] Utility function to get geographic coordinate from i and j --- .../source/scwx/qt/util/geographic_lib.cpp | 21 +++++++++++++++++++ .../source/scwx/qt/util/geographic_lib.hpp | 13 ++++++++++++ 2 files changed, 34 insertions(+) diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp index 8582d21c..20323eb8 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp @@ -1,6 +1,8 @@ #include #include +#include + #include #include #include @@ -90,6 +92,25 @@ GetAngle(double lat1, double lon1, double lat2, double lon2) return units::angle::degrees {azi1}; } +common::Coordinate GetCoordinate(const common::Coordinate& center, + units::meters i, + units::meters j) +{ + // Calculate polar coordinates based on i and j + const double angle = + std::atan2(i.value(), j.value()) * 180.0 / std::numbers::pi; + const double range = + std::sqrt(i.value() * i.value() + j.value() * j.value()); + + double latitude; + double longitude; + + DefaultGeodesic().Direct( + center.latitude_, center.longitude_, angle, range, latitude, longitude); + + return {latitude, longitude}; +} + units::length::meters GetDistance(double lat1, double lon1, double lat2, double lon2) { diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.hpp b/scwx-qt/source/scwx/qt/util/geographic_lib.hpp index 66b4f42b..7f3f64bd 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.hpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.hpp @@ -49,6 +49,19 @@ bool AreaContainsPoint(const std::vector& area, units::angle::degrees GetAngle(double lat1, double lon1, double lat2, double lon2); +/** + * Get a coordinate from an (i, j) offset. + * + * @param [in] center The center coordinate from which i and j are offset + * @param [in] i The easting offset in meters + * @param [in] j The northing offset in meters + * + * @return offset coordinate + */ +common::Coordinate GetCoordinate(const common::Coordinate& center, + units::meters i, + units::meters j); + /** * Get the distance between two points. * From 58c7b9accbdfc01b6b78e818f81bd3c582f7c738 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 10 Feb 2024 00:11:19 -0600 Subject: [PATCH 09/37] Initial LinkedVectors draw item implementation --- scwx-qt/scwx-qt.cmake | 2 + scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp | 11 + .../source/scwx/qt/gl/draw/linked_vectors.cpp | 249 ++++++++++++++++++ .../source/scwx/qt/gl/draw/linked_vectors.hpp | 147 +++++++++++ 4 files changed, 409 insertions(+) create mode 100644 scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp create mode 100644 scwx-qt/source/scwx/qt/gl/draw/linked_vectors.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index a90645e9..475b69c8 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -63,6 +63,7 @@ set(HDR_GL_DRAW source/scwx/qt/gl/draw/draw_item.hpp source/scwx/qt/gl/draw/geo_icons.hpp source/scwx/qt/gl/draw/geo_lines.hpp source/scwx/qt/gl/draw/icons.hpp + source/scwx/qt/gl/draw/linked_vectors.hpp source/scwx/qt/gl/draw/placefile_icons.hpp source/scwx/qt/gl/draw/placefile_images.hpp source/scwx/qt/gl/draw/placefile_lines.hpp @@ -74,6 +75,7 @@ set(SRC_GL_DRAW source/scwx/qt/gl/draw/draw_item.cpp source/scwx/qt/gl/draw/geo_icons.cpp source/scwx/qt/gl/draw/geo_lines.cpp source/scwx/qt/gl/draw/icons.cpp + source/scwx/qt/gl/draw/linked_vectors.cpp source/scwx/qt/gl/draw/placefile_icons.cpp source/scwx/qt/gl/draw/placefile_images.cpp source/scwx/qt/gl/draw/placefile_lines.cpp diff --git a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp index bcf6214b..32125039 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/geo_lines.cpp @@ -123,6 +123,17 @@ GeoLines::~GeoLines() = default; GeoLines::GeoLines(GeoLines&&) noexcept = default; GeoLines& GeoLines::operator=(GeoLines&&) noexcept = default; +void GeoLines::set_selected_time( + std::chrono::system_clock::time_point selectedTime) +{ + p->selectedTime_ = selectedTime; +} + +void GeoLines::set_thresholded(bool thresholded) +{ + p->thresholded_ = thresholded; +} + void GeoLines::Initialize() { gl::OpenGLFunctions& gl = p->context_->gl(); diff --git a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp new file mode 100644 index 00000000..00bcd3e3 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp @@ -0,0 +1,249 @@ +#include +#include +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace gl +{ +namespace draw +{ + +static const std::string logPrefix_ = "scwx::qt::gl::draw::linked_vectors"; + +static const boost::gil::rgba32f_pixel_t kBlack {0.0f, 0.0f, 0.0f, 1.0f}; + +struct LinkedVectorDrawItem +{ + LinkedVectorDrawItem( + const common::Coordinate& center, + const std::shared_ptr& vectorPacket) + { + coordinates_.push_back(util::GeographicLib::GetCoordinate( + center, vectorPacket->start_i_km(), vectorPacket->start_j_km())); + + const auto endI = vectorPacket->end_i_km(); + const auto endJ = vectorPacket->end_j_km(); + + std::for_each( + boost::make_zip_iterator( + boost::make_tuple(endI.begin(), endJ.begin())), + boost::make_zip_iterator(boost::make_tuple(endI.end(), endJ.end())), + [this, ¢er](const auto& p) + { + coordinates_.push_back(util::GeographicLib::GetCoordinate( + center, p.get<0>(), p.get<1>())); + }); + } + + std::vector> borderDrawItems_ {}; + std::vector> lineDrawItems_ {}; + + std::vector coordinates_ {}; + + boost::gil::rgba32f_pixel_t modulate_ {1.0f, 1.0f, 1.0f, 1.0f}; + float width_ {5.0f}; + bool visible_ {true}; + std::string hoverText_ {}; +}; + +class LinkedVectors::Impl +{ +public: + explicit Impl(std::shared_ptr context) : + context_ {context}, geoLines_ {std::make_shared(context)} + { + } + + ~Impl() {} + + std::shared_ptr context_; + + bool borderEnabled_ {true}; + bool visible_ {true}; + + std::vector> vectorList_ {}; + std::shared_ptr geoLines_; +}; + +LinkedVectors::LinkedVectors(std::shared_ptr context) : + DrawItem(context->gl()), p(std::make_unique(context)) +{ +} +LinkedVectors::~LinkedVectors() = default; + +LinkedVectors::LinkedVectors(LinkedVectors&&) noexcept = default; +LinkedVectors& LinkedVectors::operator=(LinkedVectors&&) noexcept = default; + +void LinkedVectors::set_selected_time( + std::chrono::system_clock::time_point selectedTime) +{ + p->geoLines_->set_selected_time(selectedTime); +} + +void LinkedVectors::set_thresholded(bool thresholded) +{ + p->geoLines_->set_thresholded(thresholded); +} + +void LinkedVectors::Initialize() +{ + p->geoLines_->Initialize(); +} + +void LinkedVectors::Render( + const QMapLibreGL::CustomLayerRenderParameters& params) +{ + if (!p->visible_) + { + return; + } + + p->geoLines_->Render(params); +} + +void LinkedVectors::Deinitialize() +{ + p->geoLines_->Deinitialize(); +} + +void LinkedVectors::SetBorderEnabled(bool enabled) +{ + p->borderEnabled_ = enabled; +} + +void LinkedVectors::SetVisible(bool visible) +{ + p->visible_ = visible; +} + +void LinkedVectors::StartVectors() +{ + // Start a new set of geo lines + p->geoLines_->StartLines(); +} + +std::shared_ptr LinkedVectors::AddVector( + const common::Coordinate& center, + const std::shared_ptr& vectorPacket) +{ + return p->vectorList_.emplace_back( + std::make_shared(center, vectorPacket)); +} + +void LinkedVectors::SetVectorModulate( + const std::shared_ptr& di, + boost::gil::rgba8_pixel_t modulate) +{ + di->modulate_ = {modulate[0] / 255.0f, + modulate[1] / 255.0f, + modulate[2] / 255.0f, + modulate[3] / 255.0f}; +} + +void LinkedVectors::SetVectorModulate( + const std::shared_ptr& di, + boost::gil::rgba32f_pixel_t modulate) +{ + di->modulate_ = modulate; +} + +void LinkedVectors::SetVectorWidth( + const std::shared_ptr& di, float width) +{ + di->width_ = width; +} + +void LinkedVectors::SetVectorVisible( + const std::shared_ptr& di, bool visible) +{ + di->visible_ = visible; +} + +void LinkedVectors::SetVectorHoverText( + const std::shared_ptr& di, const std::string& text) +{ + di->hoverText_ = text; +} + +void LinkedVectors::FinishVectors() +{ + // Generate borders + if (p->borderEnabled_) + { + for (auto& di : p->vectorList_) + { + for (std::size_t i = 0; i < di->coordinates_.size() - 1; ++i) + { + auto borderLine = p->geoLines_->AddLine(); + + const double& latitude1 = di->coordinates_[i].latitude_; + const double& longitude1 = di->coordinates_[i].longitude_; + const double& latitude2 = di->coordinates_[i + 1].latitude_; + const double& longitude2 = di->coordinates_[i + 1].longitude_; + + GeoLines::SetLineLocation( + borderLine, latitude1, longitude1, latitude2, longitude2); + + GeoLines::SetLineModulate(borderLine, kBlack); + GeoLines::SetLineWidth(borderLine, di->width_ + 2.0f); + GeoLines::SetLineVisible(borderLine, di->visible_); + + di->borderDrawItems_.emplace_back(std::move(borderLine)); + } + } + } + + // Generate geo lines + for (auto& di : p->vectorList_) + { + for (std::size_t i = 0; i < di->coordinates_.size() - 1; ++i) + { + auto geoLine = p->geoLines_->AddLine(); + + const double& latitude1 = di->coordinates_[i].latitude_; + const double& longitude1 = di->coordinates_[i].longitude_; + const double& latitude2 = di->coordinates_[i + 1].latitude_; + const double& longitude2 = di->coordinates_[i + 1].longitude_; + + GeoLines::SetLineLocation( + geoLine, latitude1, longitude1, latitude2, longitude2); + + GeoLines::SetLineModulate(geoLine, di->modulate_); + GeoLines::SetLineWidth(geoLine, di->width_); + GeoLines::SetLineVisible(geoLine, di->visible_); + GeoLines::SetLineHoverText(geoLine, di->hoverText_); + + di->lineDrawItems_.emplace_back(std::move(geoLine)); + } + } + + // Finish geo lines + p->geoLines_->FinishLines(); +} + +bool LinkedVectors::RunMousePicking( + const QMapLibreGL::CustomLayerRenderParameters& params, + const QPointF& mouseLocalPos, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords, + const common::Coordinate& mouseGeoCoords, + std::shared_ptr& eventHandler) +{ + return p->geoLines_->RunMousePicking(params, + mouseLocalPos, + mouseGlobalPos, + mouseCoords, + mouseGeoCoords, + eventHandler); +} + +} // namespace draw +} // namespace gl +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.hpp b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.hpp new file mode 100644 index 00000000..2dd9c898 --- /dev/null +++ b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.hpp @@ -0,0 +1,147 @@ +#pragma once + +#include +#include + +#include + +namespace scwx +{ +namespace wsr88d +{ +namespace rpg +{ + +class LinkedVectorPacket; + +} // namespace rpg +} // namespace wsr88d + +namespace qt +{ +namespace gl +{ +namespace draw +{ + +struct LinkedVectorDrawItem; + +class LinkedVectors : public DrawItem +{ +public: + explicit LinkedVectors(std::shared_ptr context); + ~LinkedVectors(); + + LinkedVectors(const LinkedVectors&) = delete; + LinkedVectors& operator=(const LinkedVectors&) = delete; + + LinkedVectors(LinkedVectors&&) noexcept; + LinkedVectors& operator=(LinkedVectors&&) noexcept; + + void set_selected_time(std::chrono::system_clock::time_point selectedTime); + void set_thresholded(bool thresholded); + + void Initialize() override; + void Render(const QMapLibreGL::CustomLayerRenderParameters& params) override; + void Deinitialize() override; + + bool + RunMousePicking(const QMapLibreGL::CustomLayerRenderParameters& params, + const QPointF& mouseLocalPos, + const QPointF& mouseGlobalPos, + const glm::vec2& mouseCoords, + const common::Coordinate& mouseGeoCoords, + std::shared_ptr& eventHandler) override; + + /** + * Enables or disables the border around each line in a linked vector. + * + * @param [in] enabled Border visibility + */ + void SetBorderEnabled(bool enabled); + + /** + * Sets the visibility of the linked vectors. + * + * @param [in] visible Line visibility + */ + void SetVisible(bool visible); + + /** + * Resets and prepares the draw item for adding a new set of linked vectors. + */ + void StartVectors(); + + /** + * Adds a linked vector to the internal draw list. + * + * @param [in] center Center coordinate on which the linked vectors are based + * @param [in] vectorPacket Linked vector packet containing start and end + * points + * + * @return Linked vector draw item + */ + std::shared_ptr AddVector( + const common::Coordinate& center, + const std::shared_ptr& vectorPacket); + + /** + * Sets the modulate color of a linked vector. + * + * @param [in] di Linked vector draw item + * @param [in] modulate Modulate color + */ + static void + SetVectorModulate(const std::shared_ptr& di, + boost::gil::rgba8_pixel_t color); + + /** + * Sets the modulate color of a linked vector. + * + * @param [in] di Linked vector draw item + * @param [in] modulate Modulate color + */ + static void + SetVectorModulate(const std::shared_ptr& di, + boost::gil::rgba32f_pixel_t modulate); + + /** + * Sets the width of the linked vector. + * + * @param [in] width Width in pixels + */ + static void SetVectorWidth(const std::shared_ptr& di, + float width); + + /** + * Sets the visibility of the linked vector. + * + * @param [in] visible + */ + static void SetVectorVisible(const std::shared_ptr& di, + bool visible); + + /** + * Sets the hover text of a linked vector. + * + * @param [in] di Linked vector draw item + * @param [in] text Hover text + */ + static void + SetVectorHoverText(const std::shared_ptr& di, + const std::string& text); + + /** + * Finalizes the draw item after adding new linked vectors. + */ + void FinishVectors(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace draw +} // namespace gl +} // namespace qt +} // namespace scwx From 2d6181b12ac2703cbdb53a121b70f7493bda34c1 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 16 Feb 2024 22:25:38 -0600 Subject: [PATCH 10/37] Remove includes from map context where only class or struct declaration is required --- scwx-qt/source/scwx/qt/map/color_table_layer.cpp | 1 + scwx-qt/source/scwx/qt/map/map_context.cpp | 2 ++ scwx-qt/source/scwx/qt/map/map_context.hpp | 12 ++++++++++-- scwx-qt/source/scwx/qt/map/map_widget.cpp | 1 + scwx-qt/source/scwx/qt/map/overlay_layer.cpp | 2 ++ scwx-qt/source/scwx/qt/map/radar_product_layer.cpp | 1 + 6 files changed, 17 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/color_table_layer.cpp b/scwx-qt/source/scwx/qt/map/color_table_layer.cpp index bd5ff9ff..eb5d5d13 100644 --- a/scwx-qt/source/scwx/qt/map/color_table_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/color_table_layer.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #if defined(_MSC_VER) diff --git a/scwx-qt/source/scwx/qt/map/map_context.cpp b/scwx-qt/source/scwx/qt/map/map_context.cpp index 79d9c719..1270a306 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.cpp +++ b/scwx-qt/source/scwx/qt/map/map_context.cpp @@ -1,4 +1,6 @@ #include +#include +#include namespace scwx { diff --git a/scwx-qt/source/scwx/qt/map/map_context.hpp b/scwx-qt/source/scwx/qt/map/map_context.hpp index fc2b893e..bdbd042f 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.hpp +++ b/scwx-qt/source/scwx/qt/map/map_context.hpp @@ -1,8 +1,7 @@ #pragma once #include -#include -#include +#include #include @@ -10,9 +9,18 @@ namespace scwx { namespace qt { +namespace view +{ + +class RadarProductView; + +} // namespace view + namespace map { +struct MapSettings; + class MapContext : public gl::GlContext { public: diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 1e65f262..42ac39c5 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include diff --git a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp index 8b18688f..6f23d8dd 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_layer.cpp @@ -3,7 +3,9 @@ #include #include #include +#include #include +#include #include #include 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 0bae51bf..964ddf00 100644 --- a/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/radar_product_layer.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include From e41e35bfc4a7439a49addd66647f9b21de6adbdd Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 16 Feb 2024 22:26:14 -0600 Subject: [PATCH 11/37] Remove duplicate selected time function --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 2 +- scwx-qt/source/scwx/qt/view/radar_product_view.cpp | 5 ----- scwx-qt/source/scwx/qt/view/radar_product_view.hpp | 2 -- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 42ac39c5..4a2bb8f8 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -442,7 +442,7 @@ std::chrono::system_clock::time_point MapWidget::GetSelectedTime() const if (radarProductView != nullptr) { // Select the time associated with the active radar product - time = radarProductView->GetSelectedTime(); + time = radarProductView->selected_time(); } return time; 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 3a7dac1c..27754821 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.cpp @@ -150,11 +150,6 @@ RadarProductView::GetDescriptionFields() const return {}; } -std::chrono::system_clock::time_point RadarProductView::GetSelectedTime() const -{ - return p->selectedTime_; -} - void RadarProductView::ComputeSweep() { logger_->debug("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 511b538a..5a2f82cd 100644 --- a/scwx-qt/source/scwx/qt/view/radar_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/radar_product_view.hpp @@ -76,8 +76,6 @@ class RadarProductView : public QObject GetDataLevelCode(std::uint16_t level) const = 0; virtual std::optional GetDataValue(std::uint16_t level) const = 0; - std::chrono::system_clock::time_point GetSelectedTime() const; - virtual std::vector> GetDescriptionFields() const; From 3ca99ca0234446b7983a3a8455e44f8ce7a875f3 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 Feb 2024 00:08:45 -0600 Subject: [PATCH 12/37] Initial implementation for overlay product view, currently only handling NST --- scwx-qt/scwx-qt.cmake | 2 + .../scwx/qt/view/overlay_product_view.cpp | 217 ++++++++++++++++++ .../scwx/qt/view/overlay_product_view.hpp | 54 +++++ 3 files changed, 273 insertions(+) create mode 100644 scwx-qt/source/scwx/qt/view/overlay_product_view.cpp create mode 100644 scwx-qt/source/scwx/qt/view/overlay_product_view.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 475b69c8..ec0979d1 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -302,12 +302,14 @@ set(HDR_VIEW source/scwx/qt/view/level2_product_view.hpp source/scwx/qt/view/level3_product_view.hpp source/scwx/qt/view/level3_radial_view.hpp source/scwx/qt/view/level3_raster_view.hpp + source/scwx/qt/view/overlay_product_view.hpp source/scwx/qt/view/radar_product_view.hpp source/scwx/qt/view/radar_product_view_factory.hpp) set(SRC_VIEW source/scwx/qt/view/level2_product_view.cpp source/scwx/qt/view/level3_product_view.cpp source/scwx/qt/view/level3_radial_view.cpp source/scwx/qt/view/level3_raster_view.cpp + source/scwx/qt/view/overlay_product_view.cpp source/scwx/qt/view/radar_product_view.cpp source/scwx/qt/view/radar_product_view_factory.cpp) diff --git a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp new file mode 100644 index 00000000..1e0085b4 --- /dev/null +++ b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp @@ -0,0 +1,217 @@ +#include +#include +#include +#include + +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace view +{ + +static const std::string logPrefix_ = "scwx::qt::view::overlay_product_view"; +static const auto logger_ = util::Logger::Create(logPrefix_); + +static const std::string kNst_ = "NST"; + +class OverlayProductView::Impl +{ +public: + explicit Impl(OverlayProductView* self) : self_ {self} {} + ~Impl() { threadPool_.join(); } + + void ConnectRadarProductManager(); + void DisconnectRadarProductManager(); + void LoadProduct(const std::string& product, + std::chrono::system_clock::time_point time, + bool autoUpdate); + void ResetProducts(); + void UpdateAutoRefresh(bool enabled) const; + + OverlayProductView* self_; + boost::uuids::uuid uuid_ {boost::uuids::random_generator()()}; + + boost::asio::thread_pool threadPool_ {1u}; + + bool autoRefreshEnabled_ {false}; + bool autoUpdateEnabled_ {false}; + + std::chrono::system_clock::time_point selectedTime_ {}; + + std::shared_ptr radarProductManager_ {nullptr}; + + std::unordered_map> + recordMap_ {}; + std::mutex recordMutex_ {}; +}; + +OverlayProductView::OverlayProductView() : p(std::make_unique(this)) {}; +OverlayProductView::~OverlayProductView() = default; + +std::shared_ptr +OverlayProductView::radar_product_manager() const +{ + return p->radarProductManager_; +} + +std::shared_ptr +OverlayProductView::radar_product_record(const std::string& product) const +{ + std::unique_lock lock {p->recordMutex_}; + + auto it = p->recordMap_.find(product); + if (it != p->recordMap_.cend()) + { + return it->second; + } + + return nullptr; +} + +std::chrono::system_clock::time_point OverlayProductView::selected_time() const +{ + return p->selectedTime_; +} + +void OverlayProductView::set_radar_product_manager( + const std::shared_ptr& radarProductManager) +{ + p->UpdateAutoRefresh(false); + p->DisconnectRadarProductManager(); + p->ResetProducts(); + + p->radarProductManager_ = radarProductManager; + + p->ConnectRadarProductManager(); + p->UpdateAutoRefresh(p->autoRefreshEnabled_); +} + +void OverlayProductView::Impl::ConnectRadarProductManager() +{ + connect( + radarProductManager_.get(), + &manager::RadarProductManager::NewDataAvailable, + self_, + [this](common::RadarProductGroup group, + const std::string& product, + std::chrono::system_clock::time_point latestTime) + { + if (autoRefreshEnabled_ && + group == common::RadarProductGroup::Level3 && product == kNst_) + { + LoadProduct(product, latestTime, autoUpdateEnabled_); + } + }, + Qt::QueuedConnection); +} + +void OverlayProductView::Impl::DisconnectRadarProductManager() +{ + if (radarProductManager_ != nullptr) + { + disconnect(radarProductManager_.get(), + &manager::RadarProductManager::NewDataAvailable, + self_, + nullptr); + } +} + +void OverlayProductView::Impl::LoadProduct( + const std::string& product, + std::chrono::system_clock::time_point time, + bool autoUpdate) +{ + logger_->debug( + "Load Product: {}, {}, {}", product, util::TimeString(time), autoUpdate); + + // Create file request + std::shared_ptr request = + std::make_shared( + radarProductManager_->radar_id()); + + if (autoUpdate) + { + connect(request.get(), + &request::NexradFileRequest::RequestComplete, + self_, + [=, this](std::shared_ptr request) + { + // Select loaded record + const auto& record = request->radar_product_record(); + + // Validate record + if (record != nullptr) + { + // Store loaded record + std::unique_lock lock {recordMutex_}; + + auto it = recordMap_.find(product); + if (it == recordMap_.cend() || it->second != record) + { + recordMap_.insert_or_assign(product, record); + + lock.unlock(); + + Q_EMIT self_->ProductUpdated(product); + } + } + }); + } + + // Load file + boost::asio::post( + threadPool_, + [=, this]() + { radarProductManager_->LoadLevel3Data(product, time, request); }); +} + +void OverlayProductView::Impl::ResetProducts() +{ + std::unique_lock lock {recordMutex_}; + recordMap_.clear(); + lock.unlock(); + + Q_EMIT self_->ProductUpdated(kNst_); +} + +void OverlayProductView::SelectTime(std::chrono::system_clock::time_point time) +{ + if (time != p->selectedTime_) + { + p->selectedTime_ = time; + p->LoadProduct(kNst_, time, true); + } +} + +void OverlayProductView::SetAutoRefresh(bool enabled) +{ + if (p->autoRefreshEnabled_ != enabled) + { + p->autoRefreshEnabled_ = enabled; + p->UpdateAutoRefresh(enabled); + } +} + +void OverlayProductView::Impl::UpdateAutoRefresh(bool enabled) const +{ + if (radarProductManager_ != nullptr) + { + radarProductManager_->EnableRefresh( + common::RadarProductGroup::Level3, kNst_, enabled, uuid_); + } +} + +void OverlayProductView::SetAutoUpdate(bool enabled) +{ + p->autoUpdateEnabled_ = enabled; +} + +} // namespace view +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/view/overlay_product_view.hpp b/scwx-qt/source/scwx/qt/view/overlay_product_view.hpp new file mode 100644 index 00000000..31e1cec0 --- /dev/null +++ b/scwx-qt/source/scwx/qt/view/overlay_product_view.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +class RadarProductManager; + +} // namespace manager + +namespace view +{ + +class OverlayProductView : public QObject +{ + Q_OBJECT + +public: + explicit OverlayProductView(); + virtual ~OverlayProductView(); + + std::shared_ptr radar_product_manager() const; + std::shared_ptr + radar_product_record(const std::string& product) const; + std::chrono::system_clock::time_point selected_time() const; + + void set_radar_product_manager( + const std::shared_ptr& radarProductManager); + + void SelectTime(std::chrono::system_clock::time_point time); + void SetAutoRefresh(bool enabled); + void SetAutoUpdate(bool enabled); + +signals: + void ProductUpdated(std::string product); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace view +} // namespace qt +} // namespace scwx From b800efd45ad180f8be69e738e2494d0dcdb45a43 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 Feb 2024 00:09:30 -0600 Subject: [PATCH 13/37] Use the linked vector border when determining whether to display hover text, in the event of a slim line --- scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp index 00bcd3e3..fad46a2a 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp @@ -193,6 +193,7 @@ void LinkedVectors::FinishVectors() GeoLines::SetLineModulate(borderLine, kBlack); GeoLines::SetLineWidth(borderLine, di->width_ + 2.0f); GeoLines::SetLineVisible(borderLine, di->visible_); + GeoLines::SetLineHoverText(borderLine, di->hoverText_); di->borderDrawItems_.emplace_back(std::move(borderLine)); } @@ -217,7 +218,12 @@ void LinkedVectors::FinishVectors() GeoLines::SetLineModulate(geoLine, di->modulate_); GeoLines::SetLineWidth(geoLine, di->width_); GeoLines::SetLineVisible(geoLine, di->visible_); - GeoLines::SetLineHoverText(geoLine, di->hoverText_); + + // If the border is not enabled, this line must have hover text instead + if (!p->borderEnabled_) + { + GeoLines::SetLineHoverText(geoLine, di->hoverText_); + } di->lineDrawItems_.emplace_back(std::move(geoLine)); } From fb06ce750583baa5db76124ffb003f5f761c9958 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 Feb 2024 00:10:00 -0600 Subject: [PATCH 14/37] Include OverlayProductView in MapContext --- scwx-qt/source/scwx/qt/map/map_context.cpp | 42 +++++++++++++--------- scwx-qt/source/scwx/qt/map/map_context.hpp | 24 +++++++------ 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/map_context.cpp b/scwx-qt/source/scwx/qt/map/map_context.cpp index 1270a306..7590d9d7 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.cpp +++ b/scwx-qt/source/scwx/qt/map/map_context.cpp @@ -1,5 +1,6 @@ #include #include +#include #include namespace scwx @@ -13,26 +14,23 @@ class MapContext::Impl { public: explicit Impl(std::shared_ptr radarProductView) : - map_ {}, - settings_ {}, - pixelRatio_ {1.0f}, - radarProductView_ {radarProductView}, - radarProductGroup_ {common::RadarProductGroup::Unknown}, - radarProduct_ {"???"}, - radarProductCode_ {0} + radarProductView_ {radarProductView} { } ~Impl() {} - std::weak_ptr map_; - MapSettings settings_; - float pixelRatio_; - std::shared_ptr radarProductView_; - common::RadarProductGroup radarProductGroup_; - std::string radarProduct_; - int16_t radarProductCode_; + std::weak_ptr map_ {}; + MapSettings settings_ {}; + float pixelRatio_ {1.0f}; + common::RadarProductGroup radarProductGroup_ { + common::RadarProductGroup::Unknown}; + std::string radarProduct_ {"???"}; + int16_t radarProductCode_ {0}; QMapLibreGL::CustomLayerRenderParameters renderParameters_ {}; + + std::shared_ptr overlayProductView_ {nullptr}; + std::shared_ptr radarProductView_; }; MapContext::MapContext( @@ -60,6 +58,12 @@ float MapContext::pixel_ratio() const return p->pixelRatio_; } +std::shared_ptr +MapContext::overlay_product_view() const +{ + return p->overlayProductView_; +} + std::shared_ptr MapContext::radar_product_view() const { return p->radarProductView_; @@ -85,18 +89,24 @@ QMapLibreGL::CustomLayerRenderParameters MapContext::render_parameters() const return p->renderParameters_; } -void MapContext::set_map(std::shared_ptr map) +void MapContext::set_map(const std::shared_ptr& map) { p->map_ = map; } +void MapContext::set_overlay_product_view( + const std::shared_ptr& overlayProductView) +{ + p->overlayProductView_ = overlayProductView; +} + void MapContext::set_pixel_ratio(float pixelRatio) { p->pixelRatio_ = pixelRatio; } void MapContext::set_radar_product_view( - std::shared_ptr radarProductView) + const std::shared_ptr& radarProductView) { p->radarProductView_ = radarProductView; } diff --git a/scwx-qt/source/scwx/qt/map/map_context.hpp b/scwx-qt/source/scwx/qt/map/map_context.hpp index bdbd042f..30498ab3 100644 --- a/scwx-qt/source/scwx/qt/map/map_context.hpp +++ b/scwx-qt/source/scwx/qt/map/map_context.hpp @@ -12,6 +12,7 @@ namespace qt namespace view { +class OverlayProductView; class RadarProductView; } // namespace view @@ -34,19 +35,22 @@ class MapContext : public gl::GlContext MapContext(MapContext&&) noexcept; MapContext& operator=(MapContext&&) noexcept; - std::weak_ptr map() const; - MapSettings& settings(); - float pixel_ratio() const; - std::shared_ptr radar_product_view() const; - common::RadarProductGroup radar_product_group() const; - std::string radar_product() const; - int16_t radar_product_code() const; - QMapLibreGL::CustomLayerRenderParameters render_parameters() const; + std::weak_ptr map() const; + MapSettings& settings(); + float pixel_ratio() const; + std::shared_ptr overlay_product_view() const; + std::shared_ptr radar_product_view() const; + common::RadarProductGroup radar_product_group() const; + std::string radar_product() const; + int16_t radar_product_code() const; + QMapLibreGL::CustomLayerRenderParameters render_parameters() const; - void set_map(std::shared_ptr map); + void set_map(const std::shared_ptr& map); + void set_overlay_product_view( + const std::shared_ptr& overlayProductView); void set_pixel_ratio(float pixelRatio); void set_radar_product_view( - std::shared_ptr radarProductView); + const std::shared_ptr& radarProductView); void set_radar_product_group(common::RadarProductGroup radarProductGroup); void set_radar_product(const std::string& radarProduct); void set_radar_product_code(int16_t radarProductCode); From 061724242b330d7d9ab2b1ae3b96b0853bff5adc Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 Feb 2024 00:10:41 -0600 Subject: [PATCH 15/37] Initialize OverlayProductView from MapWidget --- scwx-qt/source/scwx/qt/map/map_widget.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/scwx-qt/source/scwx/qt/map/map_widget.cpp b/scwx-qt/source/scwx/qt/map/map_widget.cpp index 4a2bb8f8..9d841215 100644 --- a/scwx-qt/source/scwx/qt/map/map_widget.cpp +++ b/scwx-qt/source/scwx/qt/map/map_widget.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -88,6 +89,14 @@ class MapWidgetImpl : public QObject prevBearing_ {0.0}, prevPitch_ {0.0} { + // Create views + auto overlayProductView = std::make_shared(); + overlayProductView->SetAutoRefresh(autoRefreshEnabled_); + overlayProductView->SetAutoUpdate(autoUpdateEnabled_); + + // Initialize context + context_->set_overlay_product_view(overlayProductView); + auto& generalSettings = settings::GeneralSettings::Instance(); SetRadarSite(generalSettings.default_radar_site().GetValue()); @@ -621,6 +630,9 @@ void MapWidget::SelectTime(std::chrono::system_clock::time_point time) { auto radarProductView = p->context_->radar_product_view(); + // Update other views + p->context_->overlay_product_view()->SelectTime(time); + // If there is an active radar product view if (radarProductView != nullptr) { @@ -654,12 +666,16 @@ void MapWidget::SetAutoRefresh(bool enabled) true, p->uuid_); } + + p->context_->overlay_product_view()->SetAutoRefresh(enabled); } } void MapWidget::SetAutoUpdate(bool enabled) { p->autoUpdateEnabled_ = enabled; + + p->context_->overlay_product_view()->SetAutoUpdate(enabled); } void MapWidget::SetMapLocation(double latitude, @@ -1505,6 +1521,10 @@ void MapWidgetImpl::SetRadarSite(const std::string& radarSite) // Set new RadarProductManager radarProductManager_ = manager::RadarProductManager::Instance(radarSite); + // Update views + context_->overlay_product_view()->set_radar_product_manager( + radarProductManager_); + // Connect signals to new RadarProductManager RadarProductManagerConnect(); From cd1547f573de82688328e93d26e9f7fcce775432 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 Feb 2024 00:20:05 -0600 Subject: [PATCH 16/37] When starting a new set of linked vectors, the vector list must be cleared --- scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp index fad46a2a..6843477a 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp @@ -126,6 +126,7 @@ void LinkedVectors::StartVectors() { // Start a new set of geo lines p->geoLines_->StartLines(); + p->vectorList_.clear(); } std::shared_ptr LinkedVectors::AddVector( From a0701df13fba7c0d4e896a44e435137d95c18861 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 Feb 2024 08:47:15 -0600 Subject: [PATCH 17/37] Add ticks to linked vectors --- .../source/scwx/qt/gl/draw/linked_vectors.cpp | 95 +++++++++++++++++-- .../source/scwx/qt/util/geographic_lib.cpp | 17 ++++ .../source/scwx/qt/util/geographic_lib.hpp | 17 +++- 3 files changed, 120 insertions(+), 9 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp index 6843477a..2d59b524 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp @@ -69,6 +69,10 @@ class LinkedVectors::Impl std::vector> vectorList_ {}; std::shared_ptr geoLines_; + + bool ticksEnabled_ {false}; + units::length::nautical_miles tickRadius_ {1.0}; + units::length::nautical_miles tickRadiusIncrement_ {1.0}; }; LinkedVectors::LinkedVectors(std::shared_ptr context) : @@ -179,14 +183,19 @@ void LinkedVectors::FinishVectors() { for (auto& di : p->vectorList_) { + auto tickRadius = p->tickRadius_; + for (std::size_t i = 0; i < di->coordinates_.size() - 1; ++i) { auto borderLine = p->geoLines_->AddLine(); - const double& latitude1 = di->coordinates_[i].latitude_; - const double& longitude1 = di->coordinates_[i].longitude_; - const double& latitude2 = di->coordinates_[i + 1].latitude_; - const double& longitude2 = di->coordinates_[i + 1].longitude_; + const common::Coordinate& coordinate1 = di->coordinates_[i]; + const common::Coordinate& coordinate2 = di->coordinates_[i + 1]; + + const double& latitude1 = coordinate1.latitude_; + const double& longitude1 = coordinate1.longitude_; + const double& latitude2 = coordinate2.latitude_; + const double& longitude2 = coordinate2.longitude_; GeoLines::SetLineLocation( borderLine, latitude1, longitude1, latitude2, longitude2); @@ -197,6 +206,36 @@ void LinkedVectors::FinishVectors() GeoLines::SetLineHoverText(borderLine, di->hoverText_); di->borderDrawItems_.emplace_back(std::move(borderLine)); + + if (p->ticksEnabled_) + { + auto angle = util::GeographicLib::GetAngle( + latitude1, longitude1, latitude2, longitude2); + auto angle1 = angle + units::angle::degrees(90.0); + auto angle2 = angle - units::angle::degrees(90.0); + + auto tickCoord1 = util::GeographicLib::GetCoordinate( + coordinate2, angle1, tickRadius); + auto tickCoord2 = util::GeographicLib::GetCoordinate( + coordinate2, angle2, tickRadius); + + const double& tickLat1 = tickCoord1.latitude_; + const double& tickLon1 = tickCoord1.longitude_; + const double& tickLat2 = tickCoord2.latitude_; + const double& tickLon2 = tickCoord2.longitude_; + + auto tickBorderLine = p->geoLines_->AddLine(); + + GeoLines::SetLineLocation( + tickBorderLine, tickLat1, tickLon1, tickLat2, tickLon2); + + GeoLines::SetLineModulate(tickBorderLine, kBlack); + GeoLines::SetLineWidth(tickBorderLine, di->width_ + 2.0f); + GeoLines::SetLineVisible(tickBorderLine, di->visible_); + GeoLines::SetLineHoverText(tickBorderLine, di->hoverText_); + + tickRadius += p->tickRadiusIncrement_; + } } } } @@ -204,14 +243,19 @@ void LinkedVectors::FinishVectors() // Generate geo lines for (auto& di : p->vectorList_) { + auto tickRadius = p->tickRadius_; + for (std::size_t i = 0; i < di->coordinates_.size() - 1; ++i) { auto geoLine = p->geoLines_->AddLine(); - const double& latitude1 = di->coordinates_[i].latitude_; - const double& longitude1 = di->coordinates_[i].longitude_; - const double& latitude2 = di->coordinates_[i + 1].latitude_; - const double& longitude2 = di->coordinates_[i + 1].longitude_; + const common::Coordinate& coordinate1 = di->coordinates_[i]; + const common::Coordinate& coordinate2 = di->coordinates_[i + 1]; + + const double& latitude1 = coordinate1.latitude_; + const double& longitude1 = coordinate1.longitude_; + const double& latitude2 = coordinate2.latitude_; + const double& longitude2 = coordinate2.longitude_; GeoLines::SetLineLocation( geoLine, latitude1, longitude1, latitude2, longitude2); @@ -227,6 +271,41 @@ void LinkedVectors::FinishVectors() } di->lineDrawItems_.emplace_back(std::move(geoLine)); + + if (p->ticksEnabled_) + { + auto angle = util::GeographicLib::GetAngle( + latitude1, longitude1, latitude2, longitude2); + auto angle1 = angle + units::angle::degrees(90.0); + auto angle2 = angle - units::angle::degrees(90.0); + + auto tickCoord1 = util::GeographicLib::GetCoordinate( + coordinate2, angle1, tickRadius); + auto tickCoord2 = util::GeographicLib::GetCoordinate( + coordinate2, angle2, tickRadius); + + const double& tickLat1 = tickCoord1.latitude_; + const double& tickLon1 = tickCoord1.longitude_; + const double& tickLat2 = tickCoord2.latitude_; + const double& tickLon2 = tickCoord2.longitude_; + + auto tickGeoLine = p->geoLines_->AddLine(); + + GeoLines::SetLineLocation( + tickGeoLine, tickLat1, tickLon1, tickLat2, tickLon2); + + GeoLines::SetLineModulate(tickGeoLine, di->modulate_); + GeoLines::SetLineWidth(tickGeoLine, di->width_); + GeoLines::SetLineVisible(tickGeoLine, di->visible_); + + // If the border is not enabled, this line must have hover text + if (!p->borderEnabled_) + { + GeoLines::SetLineHoverText(tickGeoLine, di->hoverText_); + } + + tickRadius += p->tickRadiusIncrement_; + } } } diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp index 20323eb8..89466975 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp @@ -92,6 +92,23 @@ GetAngle(double lat1, double lon1, double lat2, double lon2) return units::angle::degrees {azi1}; } +common::Coordinate GetCoordinate(const common::Coordinate& center, + units::angle::degrees angle, + units::length::meters distance) +{ + double latitude; + double longitude; + + DefaultGeodesic().Direct(center.latitude_, + center.longitude_, + angle.value(), + distance.value(), + latitude, + longitude); + + return {latitude, longitude}; +} + common::Coordinate GetCoordinate(const common::Coordinate& center, units::meters i, units::meters j) diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.hpp b/scwx-qt/source/scwx/qt/util/geographic_lib.hpp index 7f3f64bd..30f7a63c 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.hpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.hpp @@ -49,13 +49,28 @@ bool AreaContainsPoint(const std::vector& area, units::angle::degrees GetAngle(double lat1, double lon1, double lat2, double lon2); +/** + * Get a coordinate from a polar coordinate offset. + * + * @param [in] center The center coordinate from which the angle and distance + * are given + * @param [in] angle The angle at which the destination coordinate lies + * @param [in] distance The distance from the center coordinate to the + * destination coordinate + * + * @return offset coordinate + */ +common::Coordinate GetCoordinate(const common::Coordinate& center, + units::angle::degrees angle, + units::length::meters distance); + /** * Get a coordinate from an (i, j) offset. * * @param [in] center The center coordinate from which i and j are offset * @param [in] i The easting offset in meters * @param [in] j The northing offset in meters - * + * * @return offset coordinate */ common::Coordinate GetCoordinate(const common::Coordinate& center, From 900267b16ff29898541cd7354a12e7871fdb68c8 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 Feb 2024 23:07:25 -0600 Subject: [PATCH 18/37] Add Storm Tracking Information display to Overlay Product Layer --- .../scwx/qt/map/overlay_product_layer.cpp | 224 +++++++++++++++++- wxdata/include/scwx/wsr88d/rpg/rpg_types.hpp | 42 ++++ 2 files changed, 264 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp index 90e85372..e8ff43e9 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp @@ -1,4 +1,12 @@ #include +#include +#include +#include +#include +#include +#include +#include +#include #include namespace scwx @@ -14,13 +22,54 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class OverlayProductLayer::Impl { public: - explicit Impl(std::shared_ptr context) {} + explicit Impl(OverlayProductLayer* self, + const std::shared_ptr& context) : + self_ {self}, + linkedVectors_ {std::make_shared(context)} + { + } ~Impl() = default; + + void UpdateStormTrackingInformation(); + + static void HandleLinkedVectorPacket( + const std::shared_ptr& packet, + const common::Coordinate& center, + const std::string& hoverText, + boost::gil::rgba32f_pixel_t color, + std::shared_ptr& linkedVectors); + static void HandleScitDataPacket( + const std::shared_ptr& packet, + const common::Coordinate& center, + const std::string& stormId, + std::shared_ptr& linkedVectors); + static void + HandleStormIdPacket(const std::shared_ptr& packet, + std::string& stormId); + + OverlayProductLayer* self_; + + bool stiNeedsUpdate_ {false}; + + std::shared_ptr linkedVectors_; }; OverlayProductLayer::OverlayProductLayer(std::shared_ptr context) : - DrawLayer(context), p(std::make_unique(context)) + DrawLayer(context), p(std::make_unique(this, context)) { + auto overlayProductView = context->overlay_product_view(); + connect(overlayProductView.get(), + &view::OverlayProductView::ProductUpdated, + this, + [this](std::string product) + { + if (product == "NST") + { + p->stiNeedsUpdate_ = true; + } + }); + + AddDrawItem(p->linkedVectors_); } OverlayProductLayer::~OverlayProductLayer() = default; @@ -29,6 +78,8 @@ void OverlayProductLayer::Initialize() { logger_->debug("Initialize()"); + p->UpdateStormTrackingInformation(); + DrawLayer::Initialize(); } @@ -40,6 +91,11 @@ void OverlayProductLayer::Render( // Set OpenGL blend mode for transparency gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + if (p->stiNeedsUpdate_) + { + p->UpdateStormTrackingInformation(); + } + DrawLayer::Render(params); SCWX_GL_CHECK_ERROR(); @@ -52,6 +108,170 @@ void OverlayProductLayer::Deinitialize() DrawLayer::Deinitialize(); } +void OverlayProductLayer::Impl::UpdateStormTrackingInformation() +{ + logger_->debug("Update Storm Tracking Information"); + + stiNeedsUpdate_ = false; + + auto overlayProductView = self_->context()->overlay_product_view(); + auto radarProductManager = overlayProductView->radar_product_manager(); + auto record = overlayProductView->radar_product_record("NST"); + + float latitude = 0.0f; + float longitude = 0.0f; + + std::shared_ptr l3File = nullptr; + std::shared_ptr gpm = nullptr; + std::shared_ptr psb = nullptr; + if (record != nullptr) + { + l3File = record->level3_file(); + } + if (l3File != nullptr) + { + gpm = std::dynamic_pointer_cast( + l3File->message()); + } + if (gpm != nullptr) + { + psb = gpm->symbology_block(); + } + + linkedVectors_->StartVectors(); + + if (psb != nullptr) + { + std::shared_ptr radarSite = nullptr; + if (radarProductManager != nullptr) + { + radarSite = radarProductManager->radar_site(); + } + if (radarSite != nullptr) + { + latitude = radarSite->latitude(); + longitude = radarSite->longitude(); + } + + std::string stormId = "?"; + + for (std::size_t i = 0; i < psb->number_of_layers(); ++i) + { + auto packetList = psb->packet_list(static_cast(i)); + for (auto& packet : packetList) + { + switch (packet->packet_code()) + { + case static_cast(wsr88d::rpg::PacketCode::StormId): + HandleStormIdPacket(packet, stormId); + break; + + case static_cast( + wsr88d::rpg::PacketCode::ScitPastData): + case static_cast( + wsr88d::rpg::PacketCode::ScitForecastData): + HandleScitDataPacket( + packet, {latitude, longitude}, stormId, linkedVectors_); + break; + + default: + logger_->trace("Ignoring packet type: {}", + packet->packet_code()); + break; + } + } + } + } + else + { + logger_->trace("No Storm Tracking Information found"); + } + + linkedVectors_->FinishVectors(); +} + +void OverlayProductLayer::Impl::HandleStormIdPacket( + const std::shared_ptr& packet, std::string& stormId) +{ + auto stormIdPacket = + std::dynamic_pointer_cast(packet); + + if (stormIdPacket != nullptr && stormIdPacket->RecordCount() > 0) + { + stormId = stormIdPacket->storm_id(0); + } + else + { + logger_->warn("Invalid Storm ID Packet"); + + stormId = "?"; + } +} + +void OverlayProductLayer::Impl::HandleScitDataPacket( + const std::shared_ptr& packet, + const common::Coordinate& center, + const std::string& stormId, + std::shared_ptr& linkedVectors) +{ + auto scitDataPacket = + std::dynamic_pointer_cast(packet); + + if (scitDataPacket != nullptr) + { + boost::gil::rgba32f_pixel_t color {1.0f, 1.0f, 1.0f, 1.0f}; + if (scitDataPacket->packet_code() == + static_cast(wsr88d::rpg::PacketCode::ScitPastData)) + { + color = {0.5f, 0.5f, 0.5f, 1.0f}; + } + + for (auto& subpacket : scitDataPacket->packet_list()) + { + switch (subpacket->packet_code()) + { + case static_cast( + wsr88d::rpg::PacketCode::LinkedVectorNoValue): + HandleLinkedVectorPacket( + subpacket, center, stormId, color, linkedVectors); + break; + + default: + logger_->trace("Ignoring SCIT subpacket type: {}", + subpacket->packet_code()); + break; + } + } + } + else + { + logger_->warn("Invalid SCIT Data Packet"); + } +} + +void OverlayProductLayer::Impl::HandleLinkedVectorPacket( + const std::shared_ptr& packet, + const common::Coordinate& center, + const std::string& hoverText, + boost::gil::rgba32f_pixel_t color, + std::shared_ptr& linkedVectors) +{ + auto linkedVectorPacket = + std::dynamic_pointer_cast(packet); + + if (linkedVectorPacket != nullptr) + { + auto di = linkedVectors->AddVector(center, linkedVectorPacket); + gl::draw::LinkedVectors::SetVectorWidth(di, 1.0f); + gl::draw::LinkedVectors::SetVectorModulate(di, color); + gl::draw::LinkedVectors::SetVectorHoverText(di, hoverText); + } + else + { + logger_->warn("Invalid Linked Vector Packet"); + } +} + bool OverlayProductLayer::RunMousePicking( const QMapLibreGL::CustomLayerRenderParameters& params, const QPointF& mouseLocalPos, diff --git a/wxdata/include/scwx/wsr88d/rpg/rpg_types.hpp b/wxdata/include/scwx/wsr88d/rpg/rpg_types.hpp index a278017c..9bf2f07d 100644 --- a/wxdata/include/scwx/wsr88d/rpg/rpg_types.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/rpg_types.hpp @@ -7,6 +7,48 @@ namespace wsr88d namespace rpg { +enum class PacketCode : std::uint16_t +{ + TextNoValue = 1, + SpecialSymbol = 2, + MesocycloneSymbol3 = 3, + WindBarbData = 4, + VectorArrowData = 5, + LinkedVectorNoValue = 6, + UnlinkedVectorNoValue = 7, + TextUniform = 8, + LinkedVectorUniform = 9, + UnlinkedVectorUniform = 10, + MesocycloneSymbol11 = 11, + TornadoVortexSignatureSymbol = 12, + HailPositiveSymbol = 13, + HailProbableSymbol = 14, + StormId = 15, + DigitalRadialDataArray = 16, + DigitalPrecipitationDataArray = 17, + PrecipitationRateDataArray = 18, + HdaHailSymbol = 19, + PointFeatureSymbol = 20, + CellTrendData = 21, + CellTrendVolumeScanTimes = 22, + ScitPastData = 23, + ScitForecastData = 24, + StiCircle = 25, + ElevatedTornadoVortexSignatureSymbol = 26, + GenericData28 = 28, + GenericData29 = 29, + SetColorLevel = 0x0802, + LinkedContourVector = 0x0E03, + UnlinkedContourVector = 0x3501, + MapMessage0E23 = 0x0E23, + MapMessage3521 = 0x3521, + MapMessage4E00 = 0x4E00, + MapMessage4E01 = 0x4E01, + RadialData = 0xAF1F, + RasterDataBA07 = 0xBA07, + RasterDataBA0F = 0xBA0F +}; + enum class SpecialSymbol { PastStormCellPosition, From 7de1b1d57df94b89eff18e11e511b091cf5715ad Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 17 Feb 2024 23:35:08 -0600 Subject: [PATCH 19/37] Change tick display for past vs. forecast data --- .../source/scwx/qt/gl/draw/linked_vectors.cpp | 40 ++++++++++++++----- .../source/scwx/qt/gl/draw/linked_vectors.hpp | 31 ++++++++++++++ .../scwx/qt/map/overlay_product_layer.cpp | 28 +++++++++++-- 3 files changed, 86 insertions(+), 13 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp index 2d59b524..a457a1c4 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp @@ -50,6 +50,10 @@ struct LinkedVectorDrawItem float width_ {5.0f}; bool visible_ {true}; std::string hoverText_ {}; + + bool ticksEnabled_ {false}; + units::length::nautical_miles tickRadius_ {1.0}; + units::length::nautical_miles tickRadiusIncrement_ {0.0}; }; class LinkedVectors::Impl @@ -69,10 +73,6 @@ class LinkedVectors::Impl std::vector> vectorList_ {}; std::shared_ptr geoLines_; - - bool ticksEnabled_ {false}; - units::length::nautical_miles tickRadius_ {1.0}; - units::length::nautical_miles tickRadiusIncrement_ {1.0}; }; LinkedVectors::LinkedVectors(std::shared_ptr context) : @@ -176,6 +176,26 @@ void LinkedVectors::SetVectorHoverText( di->hoverText_ = text; } +void LinkedVectors::SetVectorTicksEnabled( + const std::shared_ptr& di, bool enabled) +{ + di->ticksEnabled_ = enabled; +} + +void LinkedVectors::SetVectorTickRadius( + const std::shared_ptr& di, + units::length::meters radius) +{ + di->tickRadius_ = radius; +} + +void LinkedVectors::SetVectorTickRadiusIncrement( + const std::shared_ptr& di, + units::length::meters radiusIncrement) +{ + di->tickRadiusIncrement_ = radiusIncrement; +} + void LinkedVectors::FinishVectors() { // Generate borders @@ -183,7 +203,7 @@ void LinkedVectors::FinishVectors() { for (auto& di : p->vectorList_) { - auto tickRadius = p->tickRadius_; + auto tickRadius = di->tickRadius_; for (std::size_t i = 0; i < di->coordinates_.size() - 1; ++i) { @@ -207,7 +227,7 @@ void LinkedVectors::FinishVectors() di->borderDrawItems_.emplace_back(std::move(borderLine)); - if (p->ticksEnabled_) + if (di->ticksEnabled_) { auto angle = util::GeographicLib::GetAngle( latitude1, longitude1, latitude2, longitude2); @@ -234,7 +254,7 @@ void LinkedVectors::FinishVectors() GeoLines::SetLineVisible(tickBorderLine, di->visible_); GeoLines::SetLineHoverText(tickBorderLine, di->hoverText_); - tickRadius += p->tickRadiusIncrement_; + tickRadius += di->tickRadiusIncrement_; } } } @@ -243,7 +263,7 @@ void LinkedVectors::FinishVectors() // Generate geo lines for (auto& di : p->vectorList_) { - auto tickRadius = p->tickRadius_; + auto tickRadius = di->tickRadius_; for (std::size_t i = 0; i < di->coordinates_.size() - 1; ++i) { @@ -272,7 +292,7 @@ void LinkedVectors::FinishVectors() di->lineDrawItems_.emplace_back(std::move(geoLine)); - if (p->ticksEnabled_) + if (di->ticksEnabled_) { auto angle = util::GeographicLib::GetAngle( latitude1, longitude1, latitude2, longitude2); @@ -304,7 +324,7 @@ void LinkedVectors::FinishVectors() GeoLines::SetLineHoverText(tickGeoLine, di->hoverText_); } - tickRadius += p->tickRadiusIncrement_; + tickRadius += di->tickRadiusIncrement_; } } } diff --git a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.hpp b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.hpp index 2dd9c898..564e3853 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.hpp @@ -4,6 +4,7 @@ #include #include +#include namespace scwx { @@ -131,6 +132,36 @@ class LinkedVectors : public DrawItem SetVectorHoverText(const std::shared_ptr& di, const std::string& text); + /** + * Sets the presence of ticks on the linked vector. + * + * @param [in] di Linked vector draw item + * @param [in] enabled Ticks enabled + */ + static void + SetVectorTicksEnabled(const std::shared_ptr& di, + bool enabled); + + /** + * Sets the tick radius of the linked vector. + * + * @param [in] di Linked vector draw item + * @param [in] radius Length of the tick extending beyond the linked vector + */ + static void + SetVectorTickRadius(const std::shared_ptr& di, + units::length::meters radius); + + /** + * Sets the tick radius increment of the linked vector. + * + * @param [in] di Linked vector draw item + * @param [in] radiusIncrement Length increment of each tick beyond the first + */ + static void + SetVectorTickRadiusIncrement(const std::shared_ptr& di, + units::length::meters radiusIncrement); + /** * Finalizes the draw item after adding new linked vectors. */ diff --git a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp index e8ff43e9..42068b36 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp @@ -37,6 +37,7 @@ class OverlayProductLayer::Impl const common::Coordinate& center, const std::string& hoverText, boost::gil::rgba32f_pixel_t color, + bool tickRadiusIncrement, std::shared_ptr& linkedVectors); static void HandleScitDataPacket( const std::shared_ptr& packet, @@ -220,10 +221,12 @@ void OverlayProductLayer::Impl::HandleScitDataPacket( if (scitDataPacket != nullptr) { boost::gil::rgba32f_pixel_t color {1.0f, 1.0f, 1.0f, 1.0f}; + bool tickRadiusIncrement = true; if (scitDataPacket->packet_code() == static_cast(wsr88d::rpg::PacketCode::ScitPastData)) { - color = {0.5f, 0.5f, 0.5f, 1.0f}; + color = {0.5f, 0.5f, 0.5f, 1.0f}; + tickRadiusIncrement = false; } for (auto& subpacket : scitDataPacket->packet_list()) @@ -232,8 +235,12 @@ void OverlayProductLayer::Impl::HandleScitDataPacket( { case static_cast( wsr88d::rpg::PacketCode::LinkedVectorNoValue): - HandleLinkedVectorPacket( - subpacket, center, stormId, color, linkedVectors); + HandleLinkedVectorPacket(subpacket, + center, + stormId, + color, + tickRadiusIncrement, + linkedVectors); break; default: @@ -254,6 +261,7 @@ void OverlayProductLayer::Impl::HandleLinkedVectorPacket( const common::Coordinate& center, const std::string& hoverText, boost::gil::rgba32f_pixel_t color, + bool tickRadiusIncrement, std::shared_ptr& linkedVectors) { auto linkedVectorPacket = @@ -265,6 +273,20 @@ void OverlayProductLayer::Impl::HandleLinkedVectorPacket( gl::draw::LinkedVectors::SetVectorWidth(di, 1.0f); gl::draw::LinkedVectors::SetVectorModulate(di, color); gl::draw::LinkedVectors::SetVectorHoverText(di, hoverText); + gl::draw::LinkedVectors::SetVectorTicksEnabled(di, true); + gl::draw::LinkedVectors::SetVectorTickRadius( + di, units::length::nautical_miles {1.0}); + + if (tickRadiusIncrement) + { + gl::draw::LinkedVectors::SetVectorTickRadiusIncrement( + di, units::length::nautical_miles {1.0}); + } + else + { + gl::draw::LinkedVectors::SetVectorTickRadiusIncrement( + di, units::length::nautical_miles {0.0}); + } } else { From 5633a30e398693a80377d2ad1fd7ace683e6c514 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 18 Feb 2024 00:06:53 -0600 Subject: [PATCH 20/37] Move overlay product layer below map symbology, improve logic for adding missing layers --- scwx-qt/source/scwx/qt/model/layer_model.cpp | 114 ++++--------------- 1 file changed, 22 insertions(+), 92 deletions(-) diff --git a/scwx-qt/source/scwx/qt/model/layer_model.cpp b/scwx-qt/source/scwx/qt/model/layer_model.cpp index 894b7281..35f188a9 100644 --- a/scwx-qt/source/scwx/qt/model/layer_model.cpp +++ b/scwx-qt/source/scwx/qt/model/layer_model.cpp @@ -43,7 +43,6 @@ static const std::vector kDefaultLayers_ { types::InformationLayer::RadarSite, false, {false, false, false, false}}, - {types::LayerType::Data, types::DataLayer::OverlayProduct, true}, {types::LayerType::Data, types::DataLayer::RadarRange, true}, {types::LayerType::Alert, awips::Phenomenon::Tornado, true}, {types::LayerType::Alert, awips::Phenomenon::SnowSquall, true}, @@ -51,6 +50,7 @@ static const std::vector kDefaultLayers_ { {types::LayerType::Alert, awips::Phenomenon::FlashFlood, true}, {types::LayerType::Alert, awips::Phenomenon::Marine, true}, {types::LayerType::Map, types::MapLayer::MapSymbology, false}, + {types::LayerType::Data, types::DataLayer::OverlayProduct, true}, {types::LayerType::Radar, std::monostate {}, true}, {types::LayerType::Map, types::MapLayer::MapUnderlay, false}, }; @@ -244,9 +244,6 @@ void LayerModel::Impl::ValidateLayerSettings(types::LayerVector& layers) // Validate immovable layers std::vector immovableIterators {}; - types::LayerVector::iterator radarSiteIterator {}; - types::LayerVector::iterator mapSymbologyIterator {}; - types::LayerVector::iterator mapUnderlayIterator {}; for (auto& immovableLayer : kImmovableLayers_) { // Set the default displayed state for a layer that is not found @@ -289,102 +286,35 @@ void LayerModel::Impl::ValidateLayerSettings(types::LayerVector& layers) it->displayed_ = displayed; } - // Store positional iterators - if (it->type_ == types::LayerType::Information) - { - switch (std::get(it->description_)) - { - case types::InformationLayer::RadarSite: - radarSiteIterator = it; - break; - - default: - break; - } - } - else if (it->type_ == types::LayerType::Map) - { - switch (std::get(it->description_)) - { - case types::MapLayer::MapSymbology: - mapSymbologyIterator = it; - break; - - case types::MapLayer::MapUnderlay: - mapUnderlayIterator = it; - break; - - default: - break; - } - } - // Add the immovable iterator to the list immovableIterators.push_back(it); } - // Validate data layers - std::vector dataIterators {}; - for (const auto& dataLayer : types::DataLayerIterator()) + // Validate the remainder of the default layer list + auto previousLayer = layers.end(); + for (auto defaultIt = kDefaultLayers_.rbegin(); + defaultIt != kDefaultLayers_.rend(); + ++defaultIt) { - // Find the data layer - auto it = std::find_if(layers.begin(), - layers.end(), - [&dataLayer](const types::LayerInfo& layer) - { - return layer.type_ == types::LayerType::Data && - std::get( - layer.description_) == dataLayer; - }); - - if (it == layers.end()) + // Find the default layer in the current layer list + auto currentIt = + std::find_if(layers.begin(), + layers.end(), + [&defaultIt](const types::LayerInfo& layer) + { + return layer.type_ == defaultIt->type_ && + layer.description_ == defaultIt->description_; + }); + + // If the default layer was not found in the current layer list + if (currentIt == layers.end()) { - // If this is the first data layer, insert after the radar site layer, - // otherwise, insert after the previous data layer - types::LayerVector::iterator insertPosition = - dataIterators.empty() ? radarSiteIterator + 1 : - dataIterators.back() + 1; - it = - layers.insert(insertPosition, {types::LayerType::Data, dataLayer}); + // Insert before the previously found layer + currentIt = layers.insert(previousLayer, *defaultIt); } - dataIterators.push_back(it); - } - - // Validate alert layers - std::vector alertIterators {}; - for (auto& phenomenon : kAlertPhenomena_) - { - // Find the alert layer - auto it = std::find_if(layers.begin(), - layers.end(), - [&phenomenon](const types::LayerInfo& layer) - { - return layer.type_ == types::LayerType::Alert && - std::get( - layer.description_) == phenomenon; - }); - - if (it == layers.end()) - { - // Insert before the map symbology layer - it = layers.insert(mapSymbologyIterator, - {types::LayerType::Alert, phenomenon}); - } - - alertIterators.push_back(it); - } - - // Validate the radar layer - auto it = std::find_if(layers.begin(), - layers.end(), - [](const types::LayerInfo& layer) - { return layer.type_ == types::LayerType::Radar; }); - if (it == layers.end()) - { - // Insert before the map underlay layer - it = layers.insert(mapUnderlayIterator, - {types::LayerType::Radar, std::monostate {}}); + // Store the current layer as the previous + previousLayer = currentIt; } } From d24918fbd5706fe084efda8fe49cf4e8a69e0eda Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 18 Feb 2024 00:07:38 -0600 Subject: [PATCH 21/37] Add test mode to use alternate settings directory --- scwx-qt/source/scwx/qt/main/main.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index ad33de07..ebc2525d 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include #include static const std::string logPrefix_ = "scwx::main"; @@ -45,6 +47,11 @@ int main(int argc, char* argv[]) QCoreApplication::installTranslator(&translator); } + if (!scwx::util::GetEnvironment("SCWX_TEST").empty()) + { + QStandardPaths::setTestModeEnabled(true); + } + // Start the io_context main loop boost::asio::io_context& ioContext = scwx::util::io_context(); auto work = boost::asio::make_work_guard(ioContext); From 09cb64b84f2b0f81b175824713cc247d9a629ddc Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 18 Feb 2024 00:48:17 -0600 Subject: [PATCH 22/37] Boost zip iterator type deduction fix for gcc --- scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp index a457a1c4..5fbdcf8c 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp @@ -34,7 +34,9 @@ struct LinkedVectorDrawItem boost::make_zip_iterator( boost::make_tuple(endI.begin(), endJ.begin())), boost::make_zip_iterator(boost::make_tuple(endI.end(), endJ.end())), - [this, ¢er](const auto& p) + [this, + ¢er](const boost::tuple, + units::length::kilometers>& p) { coordinates_.push_back(util::GeographicLib::GetCoordinate( center, p.get<0>(), p.get<1>())); From b493a8d0135f658adb5642a233cccb73d8d656d0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 18 Feb 2024 23:11:24 -0600 Subject: [PATCH 23/37] Don't display stale storm tracking information --- .../scwx/qt/view/overlay_product_view.cpp | 56 +++++++++++++++++-- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp index 1e0085b4..1bfb0f44 100644 --- a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp @@ -142,24 +142,70 @@ void OverlayProductView::Impl::LoadProduct( self_, [=, this](std::shared_ptr request) { + using namespace std::chrono_literals; + // Select loaded record const auto& record = request->radar_product_record(); // Validate record if (record != nullptr) { - // Store loaded record - std::unique_lock lock {recordMutex_}; + auto productTime = record->time(); + auto level3File = record->level3_file(); + if (level3File != nullptr) + { + const auto& header = level3File->message()->header(); + productTime = + util::TimePoint(header.date_of_message(), + header.time_of_message() * 1000); + } - auto it = recordMap_.find(product); - if (it == recordMap_.cend() || it->second != record) + // If the record is from the last 30 minutes + if (productTime + 30min >= time || + productTime + 30min >= std::chrono::system_clock::now()) { - recordMap_.insert_or_assign(product, record); + // Store loaded record + std::unique_lock lock {recordMutex_}; + auto it = recordMap_.find(product); + if (it == recordMap_.cend() || it->second != record) + { + recordMap_.insert_or_assign(product, record); + + lock.unlock(); + + Q_EMIT self_->ProductUpdated(product); + } + } + else + { + // If product is more than 30 minutes old, discard + std::unique_lock lock {recordMutex_}; + std::size_t elementsRemoved = recordMap_.erase(product); lock.unlock(); + if (elementsRemoved > 0) + { + Q_EMIT self_->ProductUpdated(product); + } + + logger_->trace("Discarding stale data: {}", + util::TimeString(productTime)); + } + } + else + { + // If the product doesn't exist, erase the stale product + std::unique_lock lock {recordMutex_}; + std::size_t elementsRemoved = recordMap_.erase(product); + lock.unlock(); + + if (elementsRemoved > 0) + { Q_EMIT self_->ProductUpdated(product); } + + logger_->trace("Removing stale product"); } }); } From 3eab1405d2228efb5472c854821281752d0bf506 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 18 Feb 2024 23:37:20 -0600 Subject: [PATCH 24/37] Use a slower retry interval if no products have been recently found, keep refresh enabled if no products were found --- .../scwx/qt/manager/radar_product_manager.cpp | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp index 732529ff..80fb4242 100644 --- a/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/radar_product_manager.cpp @@ -62,7 +62,8 @@ static constexpr uint32_t NUM_COORIDNATES_1_DEGREE = static const std::string kDefaultLevel3Product_ {"N0B"}; -static constexpr std::chrono::seconds kRetryInterval_ {15}; +static constexpr std::chrono::seconds kFastRetryInterval_ {15}; +static constexpr std::chrono::seconds kSlowRetryInterval_ {120}; static std::unordered_map> instanceMap_; @@ -638,10 +639,12 @@ void RadarProductManagerImpl::RefreshData( threadPool_, [=, this]() { + using namespace std::chrono_literals; + auto [newObjects, totalObjects] = providerManager->provider_->Refresh(); - std::chrono::milliseconds interval = kRetryInterval_; + std::chrono::milliseconds interval = kFastRetryInterval_; if (totalObjects > 0) { @@ -651,12 +654,25 @@ void RadarProductManagerImpl::RefreshData( auto updatePeriod = providerManager->provider_->update_period(); auto lastModified = providerManager->provider_->last_modified(); + auto sinceLastModified = + std::chrono::system_clock::now() - lastModified; + + // For the default interval, assume products are updated at a + // constant rate. Expect the next product at a time based on the + // previous two. interval = std::chrono::duration_cast( - updatePeriod - - (std::chrono::system_clock::now() - lastModified)); - if (interval < std::chrono::milliseconds {kRetryInterval_}) + updatePeriod - sinceLastModified); + + if (updatePeriod > 0s && sinceLastModified > updatePeriod * 5) + { + // If it has been at least 5 update periods since the file has + // been last modified, slow the retry period + interval = kSlowRetryInterval_; + } + else if (interval < std::chrono::milliseconds {kFastRetryInterval_}) { - interval = kRetryInterval_; + // The interval should be no quicker than the fast retry interval + interval = kFastRetryInterval_; } if (newObjects > 0) @@ -669,10 +685,10 @@ void RadarProductManagerImpl::RefreshData( } else if (providerManager->refreshEnabled_) { - logger_->info("[{}] No data found, disabling refresh", - providerManager->name()); + logger_->info("[{}] No data found", providerManager->name()); - providerManager->refreshEnabled_ = false; + // If no data is found, retry at the slow retry interval + interval = kSlowRetryInterval_; } if (providerManager->refreshEnabled_) From c8d8b24317f6da17dcfae91a84b72b4f951ed3b9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 19 Feb 2024 22:33:42 -0600 Subject: [PATCH 25/37] Create dedicated Storm Tracking Information message type --- .../storm_tracking_information_message.hpp | 39 +++++++++++++ .../wsr88d/rpg/level3_message_factory.cpp | 3 +- .../storm_tracking_information_message.cpp | 58 +++++++++++++++++++ wxdata/wxdata.cmake | 2 + 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp create mode 100644 wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp diff --git a/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp b/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp new file mode 100644 index 00000000..fb2a00c2 --- /dev/null +++ b/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +namespace scwx +{ +namespace wsr88d +{ +namespace rpg +{ + +class StormTrackingInformationMessage : public GraphicProductMessage +{ +public: + explicit StormTrackingInformationMessage(); + ~StormTrackingInformationMessage(); + + StormTrackingInformationMessage(const StormTrackingInformationMessage&) = + delete; + StormTrackingInformationMessage& + operator=(const StormTrackingInformationMessage&) = delete; + + StormTrackingInformationMessage(StormTrackingInformationMessage&&) noexcept; + StormTrackingInformationMessage& + operator=(StormTrackingInformationMessage&&) noexcept; + + bool Parse(std::istream& is) override; + + static std::shared_ptr + Create(Level3MessageHeader&& header, std::istream& is); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace rpg +} // namespace wsr88d +} // namespace scwx diff --git a/wxdata/source/scwx/wsr88d/rpg/level3_message_factory.cpp b/wxdata/source/scwx/wsr88d/rpg/level3_message_factory.cpp index 595f1da0..25675470 100644 --- a/wxdata/source/scwx/wsr88d/rpg/level3_message_factory.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/level3_message_factory.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -44,7 +45,7 @@ static const std::unordered_map // {51, GraphicProductMessage::Create}, {56, GraphicProductMessage::Create}, {57, GraphicProductMessage::Create}, - {58, GraphicProductMessage::Create}, + {58, StormTrackingInformationMessage::Create}, {59, GraphicProductMessage::Create}, {61, GraphicProductMessage::Create}, {62, TabularProductMessage::Create}, diff --git a/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp b/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp new file mode 100644 index 00000000..6201efd6 --- /dev/null +++ b/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp @@ -0,0 +1,58 @@ +#include +#include + +namespace scwx +{ +namespace wsr88d +{ +namespace rpg +{ + +static const std::string logPrefix_ = + "scwx::wsr88d::rpg::storm_tracking_information_message"; +static const auto logger_ = util::Logger::Create(logPrefix_); + +class StormTrackingInformationMessage::Impl +{ +public: + explicit Impl() {} + ~Impl() = default; +}; + +StormTrackingInformationMessage::StormTrackingInformationMessage() : + p(std::make_unique()) +{ +} +StormTrackingInformationMessage::~StormTrackingInformationMessage() = default; + +StormTrackingInformationMessage::StormTrackingInformationMessage( + StormTrackingInformationMessage&&) noexcept = default; +StormTrackingInformationMessage& StormTrackingInformationMessage::operator=( + StormTrackingInformationMessage&&) noexcept = default; + +bool StormTrackingInformationMessage::Parse(std::istream& is) +{ + bool dataValid = GraphicProductMessage::Parse(is); + + return dataValid; +} + +std::shared_ptr +StormTrackingInformationMessage::Create(Level3MessageHeader&& header, + std::istream& is) +{ + std::shared_ptr message = + std::make_shared(); + message->set_header(std::move(header)); + + if (!message->Parse(is)) + { + message.reset(); + } + + return message; +} + +} // namespace rpg +} // namespace wsr88d +} // namespace scwx diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 8a885719..122786fd 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -158,6 +158,7 @@ set(HDR_WSR88D_RPG include/scwx/wsr88d/rpg/ccb_header.hpp include/scwx/wsr88d/rpg/special_graphic_symbol_packet.hpp include/scwx/wsr88d/rpg/sti_circle_symbol_packet.hpp include/scwx/wsr88d/rpg/storm_id_symbol_packet.hpp + include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp include/scwx/wsr88d/rpg/tabular_alphanumeric_block.hpp include/scwx/wsr88d/rpg/tabular_product_message.hpp include/scwx/wsr88d/rpg/text_and_special_symbol_packet.hpp @@ -197,6 +198,7 @@ set(SRC_WSR88D_RPG source/scwx/wsr88d/rpg/ccb_header.cpp source/scwx/wsr88d/rpg/special_graphic_symbol_packet.cpp source/scwx/wsr88d/rpg/sti_circle_symbol_packet.cpp source/scwx/wsr88d/rpg/storm_id_symbol_packet.cpp + source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp source/scwx/wsr88d/rpg/tabular_alphanumeric_block.cpp source/scwx/wsr88d/rpg/tabular_product_message.cpp source/scwx/wsr88d/rpg/text_and_special_symbol_packet.cpp From 2cd76d7da4e66f938e38a6815c223a55ba8fac60 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 20 Feb 2024 23:26:02 -0600 Subject: [PATCH 26/37] Time and integer parsing --- wxdata/include/scwx/util/strings.hpp | 4 ++++ wxdata/include/scwx/util/time.hpp | 5 ++++ wxdata/source/scwx/util/strings.cpp | 19 ++++++++++++++++ wxdata/source/scwx/util/time.cpp | 34 ++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+) diff --git a/wxdata/include/scwx/util/strings.hpp b/wxdata/include/scwx/util/strings.hpp index f3088c9d..8aa6014d 100644 --- a/wxdata/include/scwx/util/strings.hpp +++ b/wxdata/include/scwx/util/strings.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -29,5 +30,8 @@ std::vector ParseTokens(const std::string& s, std::string ToString(const std::vector& v); +template +std::optional TryParseUnsignedLong(const std::string& str); + } // namespace util } // namespace scwx diff --git a/wxdata/include/scwx/util/time.hpp b/wxdata/include/scwx/util/time.hpp index dc58aa14..04cc1d39 100644 --- a/wxdata/include/scwx/util/time.hpp +++ b/wxdata/include/scwx/util/time.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #if !defined(_MSC_VER) # include @@ -24,5 +25,9 @@ std::string TimeString(std::chrono::system_clock::time_point time, const time_zone* timeZone = nullptr, bool epochValid = true); +template +std::optional> +TryParseDateTime(const std::string& dateTimeFormat, const std::string& str); + } // namespace util } // namespace scwx diff --git a/wxdata/source/scwx/util/strings.cpp b/wxdata/source/scwx/util/strings.cpp index e6b46a48..c17489da 100644 --- a/wxdata/source/scwx/util/strings.cpp +++ b/wxdata/source/scwx/util/strings.cpp @@ -88,5 +88,24 @@ std::string ToString(const std::vector& v) return value; } +template +std::optional TryParseUnsignedLong(const std::string& str) +{ + std::optional value = std::nullopt; + + try + { + value = static_cast(std::stoul(str)); + } + catch (const std::exception&) + { + } + + return value; +} + +template std::optional +TryParseUnsignedLong(const std::string& str); + } // namespace util } // namespace scwx diff --git a/wxdata/source/scwx/util/time.cpp b/wxdata/source/scwx/util/time.cpp index 7ab46c48..9fdd9a3e 100644 --- a/wxdata/source/scwx/util/time.cpp +++ b/wxdata/source/scwx/util/time.cpp @@ -10,6 +10,12 @@ #include +#include + +#if !defined(_MSC_VER) +# include +#endif + namespace scwx { namespace util @@ -55,5 +61,33 @@ std::string TimeString(std::chrono::system_clock::time_point time, return os.str(); } +template +std::optional> +TryParseDateTime(const std::string& dateTimeFormat, const std::string& str) +{ + using namespace std::chrono; + +#if !defined(_MSC_VER) + using namespace date; +#endif + + std::optional> value = std::nullopt; + std::chrono::sys_time dateTime; + std::istringstream ssDateTime {str}; + + ssDateTime >> parse(dateTimeFormat, dateTime); + + if (!ssDateTime.fail()) + { + value = dateTime; + } + + return value; +} + +template std::optional> +TryParseDateTime(const std::string& dateTimeFormat, + const std::string& str); + } // namespace util } // namespace scwx From 0415223571f2a9f207b972d6c7ead56eb860830a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Tue, 20 Feb 2024 23:27:08 -0600 Subject: [PATCH 27/37] Starting to parse alphanumeric data from storm tracking information --- .../storm_tracking_information_message.hpp | 27 ++++ .../wsr88d/rpg/tabular_alphanumeric_block.hpp | 2 + .../storm_tracking_information_message.cpp | 120 ++++++++++++++++++ .../wsr88d/rpg/tabular_alphanumeric_block.cpp | 8 +- 4 files changed, 156 insertions(+), 1 deletion(-) diff --git a/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp b/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp index fb2a00c2..7d238464 100644 --- a/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp @@ -2,6 +2,12 @@ #include +#include + +#include +#include +#include + namespace scwx { namespace wsr88d @@ -12,6 +18,27 @@ namespace rpg class StormTrackingInformationMessage : public GraphicProductMessage { public: + struct StiRecord + { + struct Position + { + std::optional> azimuth_ {}; + std::optional> range_ {}; + }; + + Position currentPosition_ {}; + std::optional> direction_; + std::optional> speed_; + + std::array forecastPosition_ {}; + + std::optional> forecastError_ {}; + std::optional> meanError_ {}; + + std::optional maxDbz_ {}; + std::optional> maxDbzHeight_ {}; + }; + explicit StormTrackingInformationMessage(); ~StormTrackingInformationMessage(); diff --git a/wxdata/include/scwx/wsr88d/rpg/tabular_alphanumeric_block.hpp b/wxdata/include/scwx/wsr88d/rpg/tabular_alphanumeric_block.hpp index e6ac58a8..0d41fe62 100644 --- a/wxdata/include/scwx/wsr88d/rpg/tabular_alphanumeric_block.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/tabular_alphanumeric_block.hpp @@ -31,6 +31,8 @@ class TabularAlphanumericBlock : public awips::Message size_t data_size() const override; + const std::vector>& page_list() const; + bool Parse(std::istream& is); bool Parse(std::istream& is, bool skipHeader); diff --git a/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp b/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp index 6201efd6..cc68a676 100644 --- a/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp @@ -1,5 +1,9 @@ #include #include +#include +#include + +#include namespace scwx { @@ -17,6 +21,21 @@ class StormTrackingInformationMessage::Impl public: explicit Impl() {} ~Impl() = default; + + void ParseGraphicBlock( + const std::shared_ptr& block); + void ParseTabularBlock( + const std::shared_ptr& block); + + void ParseStormPositionForecastPage(const std::vector& page); + void ParseStormCellTrackingDataPage(const std::vector& page); + + // STORM POSITION/FORECAST + std::optional radarId_ {}; + std::optional> dateTime_ {}; + std::optional numStormCells_ {}; + + std::unordered_map stiRecords_ {}; }; StormTrackingInformationMessage::StormTrackingInformationMessage() : @@ -34,9 +53,110 @@ bool StormTrackingInformationMessage::Parse(std::istream& is) { bool dataValid = GraphicProductMessage::Parse(is); + std::shared_ptr graphicBlock = nullptr; + std::shared_ptr tabularBlock = nullptr; + + if (dataValid) + { + graphicBlock = graphic_block(); + tabularBlock = tabular_block(); + } + + if (graphicBlock != nullptr) + { + p->ParseGraphicBlock(graphicBlock); + } + + if (tabularBlock != nullptr) + { + p->ParseTabularBlock(tabularBlock); + } + return dataValid; } +void StormTrackingInformationMessage::Impl::ParseGraphicBlock( + const std::shared_ptr& block) +{ + // TODO + (void) (block); +} + +void StormTrackingInformationMessage::Impl::ParseTabularBlock( + const std::shared_ptr& block) +{ + static const std::string kStormPositionForecast_ = "STORM POSITION/FORECAST"; + static const std::string kStormCellTrackingData_ = + "STORM CELL TRACKING/FORECAST ADAPTATION DATA"; + + for (auto& page : block->page_list()) + { + if (page.empty()) + { + logger_->warn("Unexpected empty page"); + continue; + } + + if (page[0].find(kStormPositionForecast_) != std::string::npos) + { + ParseStormPositionForecastPage(page); + } + else if (page[0].find(kStormCellTrackingData_) != std::string::npos) + { + ParseStormCellTrackingDataPage(page); + } + } +} + +void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( + const std::vector& page) +{ + for (std::size_t i = 1; i < page.size(); ++i) + { + const std::string& line = page[i]; + + // clang-format off + // " RADAR ID 308 DATE/TIME 12:11:21/02:15:38 NUMBER OF STORM CELLS 34" + // clang-format on + if (i == 1 && line.size() >= 74) + { + if (radarId_ == std::nullopt) + { + radarId_ = + util::TryParseUnsignedLong(line.substr(14, 3)); + } + if (dateTime_ == std::nullopt) + { + static const std::string kDateTimeFormat_ {"%m:%d:%y/%H:%M:%S"}; + + dateTime_ = util::TryParseDateTime( + kDateTimeFormat_, line.substr(29, 17)); + } + if (numStormCells_ == std::nullopt) + { + numStormCells_ = + util::TryParseUnsignedLong(line.substr(71, 3)); + } + } + // clang-format off + // " V6 183/147 234/ 63 178/137 172/129 166/122 159/117 0.7/ 0.7" + // clang-format on + else if (i >= 7 && line.size() >= 80) + { + // TODO: STI Record + std::string stormId = line.substr(2, 2); + (void) (stormId); + } + } +} + +void StormTrackingInformationMessage::Impl::ParseStormCellTrackingDataPage( + const std::vector& page) +{ + // TODO + (void) (page); +} + std::shared_ptr StormTrackingInformationMessage::Create(Level3MessageHeader&& header, std::istream& is) diff --git a/wxdata/source/scwx/wsr88d/rpg/tabular_alphanumeric_block.cpp b/wxdata/source/scwx/wsr88d/rpg/tabular_alphanumeric_block.cpp index 0fa1185a..e6c29519 100644 --- a/wxdata/source/scwx/wsr88d/rpg/tabular_alphanumeric_block.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/tabular_alphanumeric_block.cpp @@ -54,7 +54,7 @@ TabularAlphanumericBlock::TabularAlphanumericBlock() : TabularAlphanumericBlock::~TabularAlphanumericBlock() = default; TabularAlphanumericBlock::TabularAlphanumericBlock( - TabularAlphanumericBlock&&) noexcept = default; + TabularAlphanumericBlock&&) noexcept = default; TabularAlphanumericBlock& TabularAlphanumericBlock::operator=( TabularAlphanumericBlock&&) noexcept = default; @@ -68,6 +68,12 @@ size_t TabularAlphanumericBlock::data_size() const return p->lengthOfBlock_; } +const std::vector>& +TabularAlphanumericBlock::page_list() const +{ + return p->pageList_; +} + bool TabularAlphanumericBlock::Parse(std::istream& is) { return Parse(is, false); From 925f91995a4a3417aa6ea5f652ee7f9fd797ed2c Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 21 Feb 2024 22:47:50 -0600 Subject: [PATCH 28/37] Finish parsing Storm Position / Forecast page --- wxdata/include/scwx/util/strings.hpp | 2 + .../storm_tracking_information_message.hpp | 14 +-- wxdata/source/scwx/util/strings.cpp | 15 +++ .../storm_tracking_information_message.cpp | 105 +++++++++++++++++- 4 files changed, 127 insertions(+), 9 deletions(-) diff --git a/wxdata/include/scwx/util/strings.hpp b/wxdata/include/scwx/util/strings.hpp index 8aa6014d..66a1b7c6 100644 --- a/wxdata/include/scwx/util/strings.hpp +++ b/wxdata/include/scwx/util/strings.hpp @@ -30,6 +30,8 @@ std::vector ParseTokens(const std::string& s, std::string ToString(const std::vector& v); +std::optional TryParseFloat(const std::string& str); + template std::optional TryParseUnsignedLong(const std::string& str); diff --git a/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp b/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp index 7d238464..a37d0626 100644 --- a/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp @@ -22,21 +22,21 @@ class StormTrackingInformationMessage : public GraphicProductMessage { struct Position { - std::optional> azimuth_ {}; - std::optional> range_ {}; + std::optional> azimuth_ {}; + std::optional> range_ {}; }; - Position currentPosition_ {}; - std::optional> direction_; - std::optional> speed_; + Position currentPosition_ {}; + std::optional> direction_; + std::optional> speed_; std::array forecastPosition_ {}; std::optional> forecastError_ {}; std::optional> meanError_ {}; - std::optional maxDbz_ {}; - std::optional> maxDbzHeight_ {}; + std::optional maxDbz_ {}; + std::optional> maxDbzHeight_ {}; }; explicit StormTrackingInformationMessage(); diff --git a/wxdata/source/scwx/util/strings.cpp b/wxdata/source/scwx/util/strings.cpp index c17489da..dfc26177 100644 --- a/wxdata/source/scwx/util/strings.cpp +++ b/wxdata/source/scwx/util/strings.cpp @@ -88,6 +88,21 @@ std::string ToString(const std::vector& v) return value; } +std::optional TryParseFloat(const std::string& str) +{ + std::optional value = std::nullopt; + + try + { + value = static_cast(std::stof(str)); + } + catch (const std::exception&) + { + } + + return value; +} + template std::optional TryParseUnsignedLong(const std::string& str) { diff --git a/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp b/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp index cc68a676..2d598539 100644 --- a/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp @@ -143,9 +143,110 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( // clang-format on else if (i >= 7 && line.size() >= 80) { - // TODO: STI Record std::string stormId = line.substr(2, 2); - (void) (stormId); + + if (std::isupper(stormId[0]) && std::isdigit(stormId[1])) + { + auto& record = stiRecords_[stormId]; + + if (record.currentPosition_.azimuth_ == std::nullopt) + { + // Current Position: Azimuth (Degrees) + auto azimuth = + util::TryParseUnsignedLong(line.substr(9, 3)); + if (azimuth.has_value()) + { + record.currentPosition_.azimuth_ = + units::angle::degrees {azimuth.value()}; + } + } + if (record.currentPosition_.range_ == std::nullopt) + { + // Current Position: Range (Nautical Miles) + auto range = + util::TryParseUnsignedLong(line.substr(13, 3)); + if (range.has_value()) + { + record.currentPosition_.range_ = + units::length::nautical_miles { + range.value()}; + } + } + if (record.direction_ == std::nullopt) + { + // Movement: Direction (Degrees) + auto direction = + util::TryParseUnsignedLong(line.substr(19, 3)); + if (direction.has_value()) + { + record.direction_ = + units::angle::degrees {direction.value()}; + } + } + if (record.speed_ == std::nullopt) + { + // Movement: Speed (Knots) + auto speed = + util::TryParseUnsignedLong(line.substr(23, 3)); + if (speed.has_value()) + { + record.speed_ = + units::velocity::knots {speed.value()}; + } + } + for (std::size_t j = 0; j < record.forecastPosition_.size(); ++j) + { + const std::size_t positionOffset = j * 10; + + if (record.forecastPosition_[j].azimuth_ == std::nullopt) + { + // Forecast Position: Azimuth (Degrees) + std::size_t offset = 31 + positionOffset; + + auto azimuth = util::TryParseUnsignedLong( + line.substr(offset, 3)); + if (azimuth.has_value()) + { + record.forecastPosition_[j].azimuth_ = + units::angle::degrees {azimuth.value()}; + } + } + if (record.forecastPosition_[j].range_ == std::nullopt) + { + // Forecast Position: Range (Nautical Miles) + std::size_t offset = 35 + positionOffset; + + auto range = util::TryParseUnsignedLong( + line.substr(offset, 3)); + if (range.has_value()) + { + record.forecastPosition_[j].range_ = + units::length::nautical_miles { + range.value()}; + } + } + } + if (record.forecastError_ == std::nullopt) + { + // Forecast Error (Nautical Miles) + auto forecastError = util::TryParseFloat(line.substr(71, 4)); + if (forecastError.has_value()) + { + record.forecastError_ = units::length::nautical_miles { + forecastError.value()}; + } + } + if (record.meanError_ == std::nullopt) + { + // Mean Error (Nautical Miles) + auto meanError = util::TryParseFloat(line.substr(76, 4)); + if (meanError.has_value()) + { + record.meanError_ = + units::length::nautical_miles {meanError.value()}; + } + } + } } } } From 6eb9caf819aa2b90ad3d81ad3186af9beb2d84a0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 21 Feb 2024 23:02:24 -0600 Subject: [PATCH 29/37] Fix stale storm tracking information logic to use selected time --- .../scwx/qt/view/overlay_product_view.cpp | 143 +++++++++--------- 1 file changed, 72 insertions(+), 71 deletions(-) diff --git a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp index 1bfb0f44..c57e7010 100644 --- a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp @@ -137,77 +137,78 @@ void OverlayProductView::Impl::LoadProduct( if (autoUpdate) { - connect(request.get(), - &request::NexradFileRequest::RequestComplete, - self_, - [=, this](std::shared_ptr request) - { - using namespace std::chrono_literals; - - // Select loaded record - const auto& record = request->radar_product_record(); - - // Validate record - if (record != nullptr) - { - auto productTime = record->time(); - auto level3File = record->level3_file(); - if (level3File != nullptr) - { - const auto& header = level3File->message()->header(); - productTime = - util::TimePoint(header.date_of_message(), - header.time_of_message() * 1000); - } - - // If the record is from the last 30 minutes - if (productTime + 30min >= time || - productTime + 30min >= std::chrono::system_clock::now()) - { - // Store loaded record - std::unique_lock lock {recordMutex_}; - - auto it = recordMap_.find(product); - if (it == recordMap_.cend() || it->second != record) - { - recordMap_.insert_or_assign(product, record); - - lock.unlock(); - - Q_EMIT self_->ProductUpdated(product); - } - } - else - { - // If product is more than 30 minutes old, discard - std::unique_lock lock {recordMutex_}; - std::size_t elementsRemoved = recordMap_.erase(product); - lock.unlock(); - - if (elementsRemoved > 0) - { - Q_EMIT self_->ProductUpdated(product); - } - - logger_->trace("Discarding stale data: {}", - util::TimeString(productTime)); - } - } - else - { - // If the product doesn't exist, erase the stale product - std::unique_lock lock {recordMutex_}; - std::size_t elementsRemoved = recordMap_.erase(product); - lock.unlock(); - - if (elementsRemoved > 0) - { - Q_EMIT self_->ProductUpdated(product); - } - - logger_->trace("Removing stale product"); - } - }); + connect( + request.get(), + &request::NexradFileRequest::RequestComplete, + self_, + [=, this](std::shared_ptr request) + { + using namespace std::chrono_literals; + + // Select loaded record + const auto& record = request->radar_product_record(); + + // Validate record + if (record != nullptr) + { + auto productTime = record->time(); + auto level3File = record->level3_file(); + if (level3File != nullptr) + { + const auto& header = level3File->message()->header(); + productTime = util::TimePoint( + header.date_of_message(), header.time_of_message() * 1000); + } + + // If the record is from the last 30 minutes + if (productTime + 30min >= std::chrono::system_clock::now() || + (selectedTime_ != std::chrono::system_clock::time_point {} && + productTime + 30min >= selectedTime_)) + { + // Store loaded record + std::unique_lock lock {recordMutex_}; + + auto it = recordMap_.find(product); + if (it == recordMap_.cend() || it->second != record) + { + recordMap_.insert_or_assign(product, record); + + lock.unlock(); + + Q_EMIT self_->ProductUpdated(product); + } + } + else + { + // If product is more than 30 minutes old, discard + std::unique_lock lock {recordMutex_}; + std::size_t elementsRemoved = recordMap_.erase(product); + lock.unlock(); + + if (elementsRemoved > 0) + { + Q_EMIT self_->ProductUpdated(product); + } + + logger_->trace("Discarding stale data: {}", + util::TimeString(productTime)); + } + } + else + { + // If the product doesn't exist, erase the stale product + std::unique_lock lock {recordMutex_}; + std::size_t elementsRemoved = recordMap_.erase(product); + lock.unlock(); + + if (elementsRemoved > 0) + { + Q_EMIT self_->ProductUpdated(product); + } + + logger_->trace("Removing stale product"); + } + }); } // Load file From c03947d6043d56ec4fd3e4019df01fd8135f735a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 22 Feb 2024 22:32:38 -0600 Subject: [PATCH 30/37] Finish parsing storm tracking tabular block --- wxdata/include/scwx/util/strings.hpp | 10 +- wxdata/source/scwx/util/strings.cpp | 26 +-- .../storm_tracking_information_message.cpp | 192 ++++++++++++++++-- 3 files changed, 184 insertions(+), 44 deletions(-) diff --git a/wxdata/include/scwx/util/strings.hpp b/wxdata/include/scwx/util/strings.hpp index 66a1b7c6..ad0d1f9c 100644 --- a/wxdata/include/scwx/util/strings.hpp +++ b/wxdata/include/scwx/util/strings.hpp @@ -30,10 +30,14 @@ std::vector ParseTokens(const std::string& s, std::string ToString(const std::vector& v); -std::optional TryParseFloat(const std::string& str); - template -std::optional TryParseUnsignedLong(const std::string& str); +std::optional TryParseNumeric(const std::string& str); + +#if defined(STRINGS_IMPLEMENTATION) +template std::optional TryParseNumeric(const std::string& str); +template std::optional TryParseNumeric(const std::string& str); +template std::optional TryParseNumeric(const std::string& str); +#endif } // namespace util } // namespace scwx diff --git a/wxdata/source/scwx/util/strings.cpp b/wxdata/source/scwx/util/strings.cpp index dfc26177..7067d821 100644 --- a/wxdata/source/scwx/util/strings.cpp +++ b/wxdata/source/scwx/util/strings.cpp @@ -1,6 +1,9 @@ +#define STRINGS_IMPLEMENTATION + #include #include +#include namespace scwx { @@ -88,29 +91,15 @@ std::string ToString(const std::vector& v) return value; } -std::optional TryParseFloat(const std::string& str) -{ - std::optional value = std::nullopt; - - try - { - value = static_cast(std::stof(str)); - } - catch (const std::exception&) - { - } - - return value; -} - template -std::optional TryParseUnsignedLong(const std::string& str) +std::optional TryParseNumeric(const std::string& str) { std::optional value = std::nullopt; try { - value = static_cast(std::stoul(str)); + auto trimmed = boost::algorithm::trim_copy(str); + value = boost::lexical_cast(trimmed); } catch (const std::exception&) { @@ -119,8 +108,5 @@ std::optional TryParseUnsignedLong(const std::string& str) return value; } -template std::optional -TryParseUnsignedLong(const std::string& str); - } // namespace util } // namespace scwx diff --git a/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp b/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp index 2d598539..08d2e44e 100644 --- a/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp @@ -35,6 +35,24 @@ class StormTrackingInformationMessage::Impl std::optional> dateTime_ {}; std::optional numStormCells_ {}; + // STORM CELL TRACKING/FORECAST ADAPTATION DATA + std::optional> defaultDirection_ {}; + std::optional> minimumSpeed_ {}; + std::optional> defaultSpeed_ {}; + std::optional> allowableError_ {}; + std::optional maximumTime_ {}; + std::optional forecastInterval_ {}; + std::optional numberOfPastVolumes_ {}; + std::optional numberOfIntervals_ {}; + std::optional> + correlationSpeed_ {}; + std::optional errorInterval_ {}; + + // SCIT REFLECTIVITY MEDIAN FILTER + std::optional> filterKernelSize_ {}; + std::optional filterFraction_ {}; + std::optional reflectivityFiltered_ {}; + std::unordered_map stiRecords_ {}; }; @@ -122,8 +140,8 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( { if (radarId_ == std::nullopt) { - radarId_ = - util::TryParseUnsignedLong(line.substr(14, 3)); + // Radar ID (I3) + radarId_ = util::TryParseNumeric(line.substr(14, 3)); } if (dateTime_ == std::nullopt) { @@ -134,8 +152,9 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( } if (numStormCells_ == std::nullopt) { + // Number of Storm Cells (I3) numStormCells_ = - util::TryParseUnsignedLong(line.substr(71, 3)); + util::TryParseNumeric(line.substr(71, 3)); } } // clang-format off @@ -151,9 +170,9 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( if (record.currentPosition_.azimuth_ == std::nullopt) { - // Current Position: Azimuth (Degrees) + // Current Position: Azimuth (Degrees) (I3) auto azimuth = - util::TryParseUnsignedLong(line.substr(9, 3)); + util::TryParseNumeric(line.substr(9, 3)); if (azimuth.has_value()) { record.currentPosition_.azimuth_ = @@ -162,9 +181,9 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( } if (record.currentPosition_.range_ == std::nullopt) { - // Current Position: Range (Nautical Miles) + // Current Position: Range (Nautical Miles) (I3) auto range = - util::TryParseUnsignedLong(line.substr(13, 3)); + util::TryParseNumeric(line.substr(13, 3)); if (range.has_value()) { record.currentPosition_.range_ = @@ -174,9 +193,9 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( } if (record.direction_ == std::nullopt) { - // Movement: Direction (Degrees) + // Movement: Direction (Degrees) (I3) auto direction = - util::TryParseUnsignedLong(line.substr(19, 3)); + util::TryParseNumeric(line.substr(19, 3)); if (direction.has_value()) { record.direction_ = @@ -185,9 +204,9 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( } if (record.speed_ == std::nullopt) { - // Movement: Speed (Knots) + // Movement: Speed (Knots) (I3) auto speed = - util::TryParseUnsignedLong(line.substr(23, 3)); + util::TryParseNumeric(line.substr(23, 3)); if (speed.has_value()) { record.speed_ = @@ -200,10 +219,10 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( if (record.forecastPosition_[j].azimuth_ == std::nullopt) { - // Forecast Position: Azimuth (Degrees) + // Forecast Position: Azimuth (Degrees) (I3) std::size_t offset = 31 + positionOffset; - auto azimuth = util::TryParseUnsignedLong( + auto azimuth = util::TryParseNumeric( line.substr(offset, 3)); if (azimuth.has_value()) { @@ -213,10 +232,10 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( } if (record.forecastPosition_[j].range_ == std::nullopt) { - // Forecast Position: Range (Nautical Miles) + // Forecast Position: Range (Nautical Miles) (I3) std::size_t offset = 35 + positionOffset; - auto range = util::TryParseUnsignedLong( + auto range = util::TryParseNumeric( line.substr(offset, 3)); if (range.has_value()) { @@ -228,8 +247,9 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( } if (record.forecastError_ == std::nullopt) { - // Forecast Error (Nautical Miles) - auto forecastError = util::TryParseFloat(line.substr(71, 4)); + // Forecast Error (Nautical Miles) (F4.1) + auto forecastError = + util::TryParseNumeric(line.substr(71, 4)); if (forecastError.has_value()) { record.forecastError_ = units::length::nautical_miles { @@ -238,8 +258,9 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( } if (record.meanError_ == std::nullopt) { - // Mean Error (Nautical Miles) - auto meanError = util::TryParseFloat(line.substr(76, 4)); + // Mean Error (Nautical Miles) (F4.1) + auto meanError = + util::TryParseNumeric(line.substr(76, 4)); if (meanError.has_value()) { record.meanError_ = @@ -254,8 +275,137 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( void StormTrackingInformationMessage::Impl::ParseStormCellTrackingDataPage( const std::vector& page) { - // TODO - (void) (page); + for (std::size_t i = 1; i < page.size(); ++i) + { + const std::string& line = page[i]; + + // clang-format off + // " 260 (DEG) DEFAULT (DIRECTION) 2.5 (M/S) THRESH (MINIMUM SPEED)" + // clang-format on + if (i == 2 && line.size() >= 75) + { + // Default Direction (Degrees) (I3) + auto direction = + util::TryParseNumeric(line.substr(4, 3)); + if (direction.has_value()) + { + defaultDirection_ = + units::angle::degrees {direction.value()}; + } + + // Minimum Speed (Threshold) (m/s) (F4.1) + auto threshold = util::TryParseNumeric(line.substr(40, 4)); + if (threshold.has_value()) + { + minimumSpeed_ = + units::velocity::meters_per_second {threshold.value()}; + } + } + // clang-format off + // " 36.0 (KTS) DEFAULT (SPEED) 20 (KM) ALLOWABLE ERROR" + // clang-format on + if (i == 3 && line.size() >= 68) + { + // Default Speed (Knots) (F4.1) + auto speed = util::TryParseNumeric(line.substr(3, 4)); + if (speed.has_value()) + { + defaultSpeed_ = units::velocity::knots {speed.value()}; + } + + // Allowable Error (Kilometers) (I2) + auto error = util::TryParseNumeric(line.substr(42, 2)); + if (error.has_value()) + { + allowableError_ = + units::length::kilometers {error.value()}; + } + } + // clang-format off + // " 20 (MIN) TIME (MAXIMUM) 15 (MIN) FORECAST INTERVAL" + // clang-format on + if (i == 4 && line.size() >= 70) + { + // Maximum Time (Minutes) (I5) + auto time = util::TryParseNumeric(line.substr(2, 5)); + if (time.has_value()) + { + maximumTime_ = std::chrono::minutes {time.value()}; + } + + // Forecast Interval (Minutes) (I2) + auto interval = + util::TryParseNumeric(line.substr(42, 2)); + if (interval.has_value()) + { + forecastInterval_ = std::chrono::minutes {interval.value()}; + } + } + // clang-format off + // " 10 NUMBER OF PAST VOLUMES 4 NUMBER OF INTERVALS" + // clang-format on + if (i == 5 && line.size() >= 72) + { + // Number of Past Volumes (I2) + numberOfPastVolumes_ = + util::TryParseNumeric(line.substr(5, 2)); + + // Number of Intervals (I2) + numberOfIntervals_ = + util::TryParseNumeric(line.substr(42, 2)); + } + // clang-format off + // " 30.0 (M/S) CORRELATION SPEED 15 (MIN) ERROR INTERVAL" + // clang-format on + if (i == 6 && line.size() >= 67) + { + // Correlation Speed (m/s) (F4.1) + auto speed = util::TryParseNumeric(line.substr(3, 4)); + if (speed.has_value()) + { + correlationSpeed_ = + units::velocity::meters_per_second {speed.value()}; + } + + // Error Interval (Minutes) (I2) + auto interval = + util::TryParseNumeric(line.substr(42, 2)); + if (interval.has_value()) + { + errorInterval_ = std::chrono::minutes {interval.value()}; + } + } + // clang-format off + // " 7.0 (KM) FILTER KERNEL SIZE 0.5 THRESH (FILTER FRACTION)" + // clang-format on + if (i == 11 && line.size() >= 77) + { + // Filter Kernel Size (Kilometers) (F4.1) + auto kernelSize = util::TryParseNumeric(line.substr(3, 4)); + if (kernelSize.has_value()) + { + filterKernelSize_ = + units::length::kilometers {kernelSize.value()}; + } + + // Minimum Speed (Threshold) (m/s) (F4.1) + filterFraction_ = util::TryParseNumeric(line.substr(40, 4)); + } + // clang-format off + // " Yes REFLECTIVITY FILTERED" + // clang-format on + if (i == 12 && line.size() >= 37) + { + if (line.substr(4, 3) == "Yes") + { + reflectivityFiltered_ = true; + } + else if (line.substr(5, 2) == "No") + { + reflectivityFiltered_ = false; + } + } + } } std::shared_ptr From c7a9aadffa045da532841e35d11a2f5dd2cc9599 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 22 Feb 2024 23:32:58 -0600 Subject: [PATCH 31/37] Parse storm tracking graphic alphanumeric block --- .../wsr88d/rpg/graphic_alphanumeric_block.hpp | 4 + .../wsr88d/rpg/graphic_alphanumeric_block.cpp | 8 +- .../storm_tracking_information_message.cpp | 240 +++++++++++++++++- 3 files changed, 238 insertions(+), 14 deletions(-) diff --git a/wxdata/include/scwx/wsr88d/rpg/graphic_alphanumeric_block.hpp b/wxdata/include/scwx/wsr88d/rpg/graphic_alphanumeric_block.hpp index b81f5354..9b1f2019 100644 --- a/wxdata/include/scwx/wsr88d/rpg/graphic_alphanumeric_block.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/graphic_alphanumeric_block.hpp @@ -1,9 +1,11 @@ #pragma once #include +#include #include #include +#include namespace scwx { @@ -31,6 +33,8 @@ class GraphicAlphanumericBlock : public awips::Message size_t data_size() const override; + const std::vector>>& page_list() const; + bool Parse(std::istream& is); static constexpr size_t SIZE = 102u; diff --git a/wxdata/source/scwx/wsr88d/rpg/graphic_alphanumeric_block.cpp b/wxdata/source/scwx/wsr88d/rpg/graphic_alphanumeric_block.cpp index a5cc2b3a..bc9f1560 100644 --- a/wxdata/source/scwx/wsr88d/rpg/graphic_alphanumeric_block.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/graphic_alphanumeric_block.cpp @@ -44,7 +44,7 @@ GraphicAlphanumericBlock::GraphicAlphanumericBlock() : GraphicAlphanumericBlock::~GraphicAlphanumericBlock() = default; GraphicAlphanumericBlock::GraphicAlphanumericBlock( - GraphicAlphanumericBlock&&) noexcept = default; + GraphicAlphanumericBlock&&) noexcept = default; GraphicAlphanumericBlock& GraphicAlphanumericBlock::operator=( GraphicAlphanumericBlock&&) noexcept = default; @@ -58,6 +58,12 @@ size_t GraphicAlphanumericBlock::data_size() const return p->lengthOfBlock_; } +const std::vector>>& +GraphicAlphanumericBlock::page_list() const +{ + return p->pageList_; +} + bool GraphicAlphanumericBlock::Parse(std::istream& is) { bool blockValid = true; diff --git a/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp b/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp index 08d2e44e..b39035a2 100644 --- a/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -30,6 +32,9 @@ class StormTrackingInformationMessage::Impl void ParseStormPositionForecastPage(const std::vector& page); void ParseStormCellTrackingDataPage(const std::vector& page); + void HandleTextUniformPacket(std::shared_ptr packet, + std::vector& stormIds); + // STORM POSITION/FORECAST std::optional radarId_ {}; std::optional> dateTime_ {}; @@ -96,8 +101,217 @@ bool StormTrackingInformationMessage::Parse(std::istream& is) void StormTrackingInformationMessage::Impl::ParseGraphicBlock( const std::shared_ptr& block) { - // TODO - (void) (block); + for (auto& page : block->page_list()) + { + std::vector stormIds {}; + + for (auto& packet : page) + { + switch (packet->packet_code()) + { + case static_cast(wsr88d::rpg::PacketCode::TextUniform): + HandleTextUniformPacket(packet, stormIds); + break; + + default: + logger_->trace("Ignoring graphic alphanumeric packet type: {}", + packet->packet_code()); + break; + } + } + } +} + +void StormTrackingInformationMessage::Impl::HandleTextUniformPacket( + std::shared_ptr packet, std::vector& stormIds) +{ + auto textPacket = + std::dynamic_pointer_cast( + packet); + + if (textPacket != nullptr && textPacket->text().size() >= 69) + { + auto text = textPacket->text(); + + // " STORM ID D7 H3 K5 N5 U6 E7" + if (text.starts_with(" STORM ID")) + { + static constexpr std::size_t kMaxStormIds = 6; + static constexpr std::size_t kStartOffset = 17; + static constexpr std::size_t kStride = 10; + + stormIds.clear(); + + for (std::size_t i = 0, offset = kStartOffset; i < kMaxStormIds; + ++i, offset += kStride) + { + std::string stormId = text.substr(offset, 2); + + if (std::isupper(stormId[0]) && std::isdigit(stormId[1])) + { + stormIds.push_back(stormId); + } + } + } + + // " AZ/RAN 242/ 77 45/ 36 180/139 175/126 23/110 25/ 83" + else if (text.starts_with(" AZ/RAN")) + { + static constexpr std::size_t kAzStartOffset = 11; + static constexpr std::size_t kRanStartOffset = 15; + static constexpr std::size_t kStride = 10; + + for (std::size_t i = 0, + azOffset = kAzStartOffset, + ranOffset = kRanStartOffset; + i < stormIds.size(); + ++i, azOffset += kStride, ranOffset += kStride) + { + auto& record = stiRecords_[stormIds[i]]; + + if (!record.currentPosition_.azimuth_.has_value()) + { + // Current Position: Azimuth (Degrees) (I3) + auto azimuth = util::TryParseNumeric( + text.substr(azOffset, 3)); + if (azimuth.has_value()) + { + record.currentPosition_.azimuth_ = + units::angle::degrees {azimuth.value()}; + } + } + + if (!record.currentPosition_.range_.has_value()) + { + // Current Position: Range (Nautical Miles) (I3) + auto range = util::TryParseNumeric( + text.substr(ranOffset, 3)); + if (range.has_value()) + { + record.currentPosition_.range_ = + units::length::nautical_miles { + range.value()}; + } + } + } + } + + // " FCST MVT 262/ 56 249/ 48 234/ 46 228/ 48 227/ 66 242/ 48" + else if (text.starts_with(" FCST MVT")) + { + static constexpr std::size_t kDirStartOffset = 11; + static constexpr std::size_t kSpeedStartOffset = 15; + static constexpr std::size_t kStride = 10; + + for (std::size_t i = 0, + dirOffset = kDirStartOffset, + speedOffset = kSpeedStartOffset; + i < stormIds.size(); + ++i, dirOffset += kStride, speedOffset += kStride) + { + auto& record = stiRecords_[stormIds[i]]; + + if (!record.direction_.has_value()) + { + // Movement: Direction (Degrees) (I3) + auto direction = util::TryParseNumeric( + text.substr(dirOffset, 3)); + if (direction.has_value()) + { + record.direction_ = + units::angle::degrees {direction.value()}; + } + } + + if (!record.speed_.has_value()) + { + // Movement: Speed (Knots) (I3) + auto speed = util::TryParseNumeric( + text.substr(speedOffset, 3)); + if (speed.has_value()) + { + record.speed_ = + units::velocity::knots {speed.value()}; + } + } + } + } + + // " ERR/MEAN 4.5/ 2.9 0.8/ 1.7 1.4/ 1.4 1.3/ 1.3 1.4/ 1.7 1.2/ 0.8" + else if (text.starts_with(" ERR/MEAN")) + { + static constexpr std::size_t kErrStartOffset = 10; + static constexpr std::size_t kMeanStartOffset = 15; + static constexpr std::size_t kStride = 10; + + for (std::size_t i = 0, + errOffset = kErrStartOffset, + meanOffset = kMeanStartOffset; + i < stormIds.size(); + ++i, errOffset += kStride, meanOffset += kStride) + { + auto& record = stiRecords_[stormIds[i]]; + + if (!record.forecastError_.has_value()) + { + // Forecast Error (Nautical Miles) (F4.1) + auto forecastError = + util::TryParseNumeric(text.substr(errOffset, 4)); + if (forecastError.has_value()) + { + record.forecastError_ = units::length::nautical_miles { + forecastError.value()}; + } + } + + if (!record.meanError_.has_value()) + { + // Mean Error (Nautical Miles) (F4.1) + auto meanError = + util::TryParseNumeric(text.substr(meanOffset, 4)); + if (meanError.has_value()) + { + record.meanError_ = + units::length::nautical_miles {meanError.value()}; + } + } + } + } + + // " DBZM HGT 55 7.5 56 4.2 48 20.6 51 17.4 51 14.0 54 8.9" + else if (text.starts_with(" DBZM HGT")) + { + static constexpr std::size_t kDbzmStartOffset = 12; + static constexpr std::size_t kHgtStartOffset = 15; + static constexpr std::size_t kStride = 10; + + for (std::size_t i = 0, + dbzmOffset = kDbzmStartOffset, + hgtOffset = kHgtStartOffset; + i < stormIds.size(); + ++i, dbzmOffset += kStride, hgtOffset += kStride) + { + auto& record = stiRecords_[stormIds[i]]; + + // Maximum dBZ (I2) + record.maxDbz_ = + util::TryParseNumeric(text.substr(dbzmOffset, 2)); + + // Maximum dBZ Height (Feet) (F4.1) + auto height = + util::TryParseNumeric(text.substr(hgtOffset, 4)); + if (height.has_value()) + { + record.maxDbzHeight_ = units::length::feet { + static_cast(height.value() * 1000.0f)}; + } + } + } + } + else + { + logger_->warn("Invalid Text Uniform Packet"); + } } void StormTrackingInformationMessage::Impl::ParseTabularBlock( @@ -138,19 +352,19 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( // clang-format on if (i == 1 && line.size() >= 74) { - if (radarId_ == std::nullopt) + if (!radarId_.has_value()) { // Radar ID (I3) radarId_ = util::TryParseNumeric(line.substr(14, 3)); } - if (dateTime_ == std::nullopt) + if (!dateTime_.has_value()) { static const std::string kDateTimeFormat_ {"%m:%d:%y/%H:%M:%S"}; dateTime_ = util::TryParseDateTime( kDateTimeFormat_, line.substr(29, 17)); } - if (numStormCells_ == std::nullopt) + if (!numStormCells_.has_value()) { // Number of Storm Cells (I3) numStormCells_ = @@ -168,7 +382,7 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( { auto& record = stiRecords_[stormId]; - if (record.currentPosition_.azimuth_ == std::nullopt) + if (!record.currentPosition_.azimuth_.has_value()) { // Current Position: Azimuth (Degrees) (I3) auto azimuth = @@ -179,7 +393,7 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( units::angle::degrees {azimuth.value()}; } } - if (record.currentPosition_.range_ == std::nullopt) + if (!record.currentPosition_.range_.has_value()) { // Current Position: Range (Nautical Miles) (I3) auto range = @@ -191,7 +405,7 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( range.value()}; } } - if (record.direction_ == std::nullopt) + if (!record.direction_.has_value()) { // Movement: Direction (Degrees) (I3) auto direction = @@ -202,7 +416,7 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( units::angle::degrees {direction.value()}; } } - if (record.speed_ == std::nullopt) + if (!record.speed_.has_value()) { // Movement: Speed (Knots) (I3) auto speed = @@ -217,7 +431,7 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( { const std::size_t positionOffset = j * 10; - if (record.forecastPosition_[j].azimuth_ == std::nullopt) + if (!record.forecastPosition_[j].azimuth_.has_value()) { // Forecast Position: Azimuth (Degrees) (I3) std::size_t offset = 31 + positionOffset; @@ -230,7 +444,7 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( units::angle::degrees {azimuth.value()}; } } - if (record.forecastPosition_[j].range_ == std::nullopt) + if (!record.forecastPosition_[j].range_.has_value()) { // Forecast Position: Range (Nautical Miles) (I3) std::size_t offset = 35 + positionOffset; @@ -245,7 +459,7 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( } } } - if (record.forecastError_ == std::nullopt) + if (!record.forecastError_.has_value()) { // Forecast Error (Nautical Miles) (F4.1) auto forecastError = @@ -256,7 +470,7 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( forecastError.value()}; } } - if (record.meanError_ == std::nullopt) + if (!record.meanError_.has_value()) { // Mean Error (Nautical Miles) (F4.1) auto meanError = From b4b170658773ec7d7e9a0aacfd202ecfdd3076b7 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 23 Feb 2024 22:49:17 -0600 Subject: [PATCH 32/37] Expose Storm Tracking Information in interface --- .../storm_tracking_information_message.hpp | 26 +++ .../storm_tracking_information_message.cpp | 214 ++++++++++++++---- 2 files changed, 195 insertions(+), 45 deletions(-) diff --git a/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp b/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp index a37d0626..6dab5427 100644 --- a/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp +++ b/wxdata/include/scwx/wsr88d/rpg/storm_tracking_information_message.hpp @@ -51,6 +51,32 @@ class StormTrackingInformationMessage : public GraphicProductMessage StormTrackingInformationMessage& operator=(StormTrackingInformationMessage&&) noexcept; + std::optional radar_id() const; + std::optional> date_time() const; + std::optional num_storm_cells() const; + + std::optional> + default_direction() const; + std::optional> + minimum_speed() const; + std::optional> default_speed() const; + std::optional> + allowable_error() const; + std::optional maximum_time() const; + std::optional forecast_interval() const; + std::optional number_of_past_volumes() const; + std::optional number_of_intervals() const; + std::optional> + correlation_speed() const; + std::optional error_interval() const; + + std::optional> filter_kernel_size() const; + std::optional filter_fraction() const; + std::optional reflectivity_filtered() const; + + std::shared_ptr + sti_record(const std::string& stormId) const; + bool Parse(std::istream& is) override; static std::shared_ptr diff --git a/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp b/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp index b39035a2..b61b05c8 100644 --- a/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp @@ -24,6 +24,8 @@ class StormTrackingInformationMessage::Impl explicit Impl() {} ~Impl() = default; + std::shared_ptr& GetOrCreateStiRecord(const std::string& stormId); + void ParseGraphicBlock( const std::shared_ptr& block); void ParseTabularBlock( @@ -58,7 +60,7 @@ class StormTrackingInformationMessage::Impl std::optional filterFraction_ {}; std::optional reflectivityFiltered_ {}; - std::unordered_map stiRecords_ {}; + std::unordered_map> stiRecords_ {}; }; StormTrackingInformationMessage::StormTrackingInformationMessage() : @@ -72,6 +74,126 @@ StormTrackingInformationMessage::StormTrackingInformationMessage( StormTrackingInformationMessage& StormTrackingInformationMessage::operator=( StormTrackingInformationMessage&&) noexcept = default; +std::optional StormTrackingInformationMessage::radar_id() const +{ + return p->radarId_; +} + +std::optional> +StormTrackingInformationMessage::date_time() const +{ + return p->dateTime_; +} + +std::optional +StormTrackingInformationMessage::num_storm_cells() const +{ + return p->numStormCells_; +} + +std::optional> +StormTrackingInformationMessage::default_direction() const +{ + return p->defaultDirection_; +} + +std::optional> +StormTrackingInformationMessage::minimum_speed() const +{ + return p->minimumSpeed_; +} + +std::optional> +StormTrackingInformationMessage::default_speed() const +{ + return p->defaultSpeed_; +} + +std::optional> +StormTrackingInformationMessage::allowable_error() const +{ + return p->allowableError_; +} + +std::optional +StormTrackingInformationMessage::maximum_time() const +{ + return p->maximumTime_; +} + +std::optional +StormTrackingInformationMessage::forecast_interval() const +{ + return p->forecastInterval_; +} + +std::optional +StormTrackingInformationMessage::number_of_past_volumes() const +{ + return p->numberOfPastVolumes_; +} + +std::optional +StormTrackingInformationMessage::number_of_intervals() const +{ + return p->numberOfIntervals_; +} + +std::optional> +StormTrackingInformationMessage::correlation_speed() const +{ + return p->correlationSpeed_; +} + +std::optional +StormTrackingInformationMessage::error_interval() const +{ + return p->errorInterval_; +} + +std::optional> +StormTrackingInformationMessage::filter_kernel_size() const +{ + return p->filterKernelSize_; +} + +std::optional StormTrackingInformationMessage::filter_fraction() const +{ + return p->filterFraction_; +} + +std::optional +StormTrackingInformationMessage::reflectivity_filtered() const +{ + return p->reflectivityFiltered_; +} + +std::shared_ptr +StormTrackingInformationMessage::sti_record(const std::string& stormId) const +{ + std::shared_ptr record = nullptr; + + auto it = p->stiRecords_.find(stormId); + if (it != p->stiRecords_.cend()) + { + record = it->second; + } + + return record; +} + +std::shared_ptr& +StormTrackingInformationMessage::Impl::GetOrCreateStiRecord( + const std::string& stormId) +{ + auto& record = stiRecords_[stormId]; + if (record == nullptr) + { + record = std::make_shared(); + } + return record; +} + bool StormTrackingInformationMessage::Parse(std::istream& is) { bool dataValid = GraphicProductMessage::Parse(is); @@ -167,28 +289,28 @@ void StormTrackingInformationMessage::Impl::HandleTextUniformPacket( i < stormIds.size(); ++i, azOffset += kStride, ranOffset += kStride) { - auto& record = stiRecords_[stormIds[i]]; + auto& record = GetOrCreateStiRecord(stormIds[i]); - if (!record.currentPosition_.azimuth_.has_value()) + if (!record->currentPosition_.azimuth_.has_value()) { // Current Position: Azimuth (Degrees) (I3) auto azimuth = util::TryParseNumeric( text.substr(azOffset, 3)); if (azimuth.has_value()) { - record.currentPosition_.azimuth_ = + record->currentPosition_.azimuth_ = units::angle::degrees {azimuth.value()}; } } - if (!record.currentPosition_.range_.has_value()) + if (!record->currentPosition_.range_.has_value()) { // Current Position: Range (Nautical Miles) (I3) auto range = util::TryParseNumeric( text.substr(ranOffset, 3)); if (range.has_value()) { - record.currentPosition_.range_ = + record->currentPosition_.range_ = units::length::nautical_miles { range.value()}; } @@ -209,28 +331,28 @@ void StormTrackingInformationMessage::Impl::HandleTextUniformPacket( i < stormIds.size(); ++i, dirOffset += kStride, speedOffset += kStride) { - auto& record = stiRecords_[stormIds[i]]; + auto& record = GetOrCreateStiRecord(stormIds[i]); - if (!record.direction_.has_value()) + if (!record->direction_.has_value()) { // Movement: Direction (Degrees) (I3) auto direction = util::TryParseNumeric( text.substr(dirOffset, 3)); if (direction.has_value()) { - record.direction_ = + record->direction_ = units::angle::degrees {direction.value()}; } } - if (!record.speed_.has_value()) + if (!record->speed_.has_value()) { // Movement: Speed (Knots) (I3) auto speed = util::TryParseNumeric( text.substr(speedOffset, 3)); if (speed.has_value()) { - record.speed_ = + record->speed_ = units::velocity::knots {speed.value()}; } } @@ -250,28 +372,29 @@ void StormTrackingInformationMessage::Impl::HandleTextUniformPacket( i < stormIds.size(); ++i, errOffset += kStride, meanOffset += kStride) { - auto& record = stiRecords_[stormIds[i]]; + auto& record = GetOrCreateStiRecord(stormIds[i]); - if (!record.forecastError_.has_value()) + if (!record->forecastError_.has_value()) { // Forecast Error (Nautical Miles) (F4.1) auto forecastError = util::TryParseNumeric(text.substr(errOffset, 4)); if (forecastError.has_value()) { - record.forecastError_ = units::length::nautical_miles { - forecastError.value()}; + record->forecastError_ = + units::length::nautical_miles { + forecastError.value()}; } } - if (!record.meanError_.has_value()) + if (!record->meanError_.has_value()) { // Mean Error (Nautical Miles) (F4.1) auto meanError = util::TryParseNumeric(text.substr(meanOffset, 4)); if (meanError.has_value()) { - record.meanError_ = + record->meanError_ = units::length::nautical_miles {meanError.value()}; } } @@ -291,10 +414,10 @@ void StormTrackingInformationMessage::Impl::HandleTextUniformPacket( i < stormIds.size(); ++i, dbzmOffset += kStride, hgtOffset += kStride) { - auto& record = stiRecords_[stormIds[i]]; + auto& record = GetOrCreateStiRecord(stormIds[i]); // Maximum dBZ (I2) - record.maxDbz_ = + record->maxDbz_ = util::TryParseNumeric(text.substr(dbzmOffset, 2)); // Maximum dBZ Height (Feet) (F4.1) @@ -302,7 +425,7 @@ void StormTrackingInformationMessage::Impl::HandleTextUniformPacket( util::TryParseNumeric(text.substr(hgtOffset, 4)); if (height.has_value()) { - record.maxDbzHeight_ = units::length::feet { + record->maxDbzHeight_ = units::length::feet { static_cast(height.value() * 1000.0f)}; } } @@ -380,58 +503,58 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( if (std::isupper(stormId[0]) && std::isdigit(stormId[1])) { - auto& record = stiRecords_[stormId]; + auto& record = GetOrCreateStiRecord(stormId); - if (!record.currentPosition_.azimuth_.has_value()) + if (!record->currentPosition_.azimuth_.has_value()) { // Current Position: Azimuth (Degrees) (I3) auto azimuth = util::TryParseNumeric(line.substr(9, 3)); if (azimuth.has_value()) { - record.currentPosition_.azimuth_ = + record->currentPosition_.azimuth_ = units::angle::degrees {azimuth.value()}; } } - if (!record.currentPosition_.range_.has_value()) + if (!record->currentPosition_.range_.has_value()) { // Current Position: Range (Nautical Miles) (I3) auto range = util::TryParseNumeric(line.substr(13, 3)); if (range.has_value()) { - record.currentPosition_.range_ = + record->currentPosition_.range_ = units::length::nautical_miles { range.value()}; } } - if (!record.direction_.has_value()) + if (!record->direction_.has_value()) { // Movement: Direction (Degrees) (I3) auto direction = util::TryParseNumeric(line.substr(19, 3)); if (direction.has_value()) { - record.direction_ = + record->direction_ = units::angle::degrees {direction.value()}; } } - if (!record.speed_.has_value()) + if (!record->speed_.has_value()) { // Movement: Speed (Knots) (I3) auto speed = util::TryParseNumeric(line.substr(23, 3)); if (speed.has_value()) { - record.speed_ = + record->speed_ = units::velocity::knots {speed.value()}; } } - for (std::size_t j = 0; j < record.forecastPosition_.size(); ++j) + for (std::size_t j = 0; j < record->forecastPosition_.size(); ++j) { const std::size_t positionOffset = j * 10; - if (!record.forecastPosition_[j].azimuth_.has_value()) + if (!record->forecastPosition_[j].azimuth_.has_value()) { // Forecast Position: Azimuth (Degrees) (I3) std::size_t offset = 31 + positionOffset; @@ -440,11 +563,11 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( line.substr(offset, 3)); if (azimuth.has_value()) { - record.forecastPosition_[j].azimuth_ = + record->forecastPosition_[j].azimuth_ = units::angle::degrees {azimuth.value()}; } } - if (!record.forecastPosition_[j].range_.has_value()) + if (!record->forecastPosition_[j].range_.has_value()) { // Forecast Position: Range (Nautical Miles) (I3) std::size_t offset = 35 + positionOffset; @@ -453,31 +576,32 @@ void StormTrackingInformationMessage::Impl::ParseStormPositionForecastPage( line.substr(offset, 3)); if (range.has_value()) { - record.forecastPosition_[j].range_ = + record->forecastPosition_[j].range_ = units::length::nautical_miles { range.value()}; } } } - if (!record.forecastError_.has_value()) + if (!record->forecastError_.has_value()) { // Forecast Error (Nautical Miles) (F4.1) auto forecastError = util::TryParseNumeric(line.substr(71, 4)); if (forecastError.has_value()) { - record.forecastError_ = units::length::nautical_miles { - forecastError.value()}; + record->forecastError_ = + units::length::nautical_miles { + forecastError.value()}; } } - if (!record.meanError_.has_value()) + if (!record->meanError_.has_value()) { // Mean Error (Nautical Miles) (F4.1) auto meanError = util::TryParseNumeric(line.substr(76, 4)); if (meanError.has_value()) { - record.meanError_ = + record->meanError_ = units::length::nautical_miles {meanError.value()}; } } @@ -515,9 +639,8 @@ void StormTrackingInformationMessage::Impl::ParseStormCellTrackingDataPage( units::velocity::meters_per_second {threshold.value()}; } } - // clang-format off + // " 36.0 (KTS) DEFAULT (SPEED) 20 (KM) ALLOWABLE ERROR" - // clang-format on if (i == 3 && line.size() >= 68) { // Default Speed (Knots) (F4.1) @@ -535,6 +658,7 @@ void StormTrackingInformationMessage::Impl::ParseStormCellTrackingDataPage( units::length::kilometers {error.value()}; } } + // clang-format off // " 20 (MIN) TIME (MAXIMUM) 15 (MIN) FORECAST INTERVAL" // clang-format on @@ -555,6 +679,7 @@ void StormTrackingInformationMessage::Impl::ParseStormCellTrackingDataPage( forecastInterval_ = std::chrono::minutes {interval.value()}; } } + // clang-format off // " 10 NUMBER OF PAST VOLUMES 4 NUMBER OF INTERVALS" // clang-format on @@ -568,9 +693,8 @@ void StormTrackingInformationMessage::Impl::ParseStormCellTrackingDataPage( numberOfIntervals_ = util::TryParseNumeric(line.substr(42, 2)); } - // clang-format off + // " 30.0 (M/S) CORRELATION SPEED 15 (MIN) ERROR INTERVAL" - // clang-format on if (i == 6 && line.size() >= 67) { // Correlation Speed (m/s) (F4.1) @@ -589,6 +713,7 @@ void StormTrackingInformationMessage::Impl::ParseStormCellTrackingDataPage( errorInterval_ = std::chrono::minutes {interval.value()}; } } + // clang-format off // " 7.0 (KM) FILTER KERNEL SIZE 0.5 THRESH (FILTER FRACTION)" // clang-format on @@ -605,9 +730,8 @@ void StormTrackingInformationMessage::Impl::ParseStormCellTrackingDataPage( // Minimum Speed (Threshold) (m/s) (F4.1) filterFraction_ = util::TryParseNumeric(line.substr(40, 4)); } - // clang-format off + // " Yes REFLECTIVITY FILTERED" - // clang-format on if (i == 12 && line.size() >= 37) { if (line.substr(4, 3) == "Yes") From 29e87fc11e820a4926a0816877797a58b35c3cd4 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 23 Feb 2024 23:53:59 -0600 Subject: [PATCH 33/37] Generate hover text for storm tracking information --- .../source/scwx/qt/gl/draw/linked_vectors.cpp | 9 +- .../source/scwx/qt/gl/draw/linked_vectors.hpp | 7 +- .../scwx/qt/map/overlay_product_layer.cpp | 199 +++++++++++++----- 3 files changed, 155 insertions(+), 60 deletions(-) diff --git a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp index 5fbdcf8c..9e4b664f 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp +++ b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.cpp @@ -21,8 +21,9 @@ static const boost::gil::rgba32f_pixel_t kBlack {0.0f, 0.0f, 0.0f, 1.0f}; struct LinkedVectorDrawItem { LinkedVectorDrawItem( - const common::Coordinate& center, - const std::shared_ptr& vectorPacket) + const common::Coordinate& center, + const std::shared_ptr& + vectorPacket) { coordinates_.push_back(util::GeographicLib::GetCoordinate( center, vectorPacket->start_i_km(), vectorPacket->start_j_km())); @@ -136,8 +137,8 @@ void LinkedVectors::StartVectors() } std::shared_ptr LinkedVectors::AddVector( - const common::Coordinate& center, - const std::shared_ptr& vectorPacket) + const common::Coordinate& center, + const std::shared_ptr& vectorPacket) { return p->vectorList_.emplace_back( std::make_shared(center, vectorPacket)); diff --git a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.hpp b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.hpp index 564e3853..9e8eef6c 100644 --- a/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.hpp +++ b/scwx-qt/source/scwx/qt/gl/draw/linked_vectors.hpp @@ -82,9 +82,10 @@ class LinkedVectors : public DrawItem * * @return Linked vector draw item */ - std::shared_ptr AddVector( - const common::Coordinate& center, - const std::shared_ptr& vectorPacket); + std::shared_ptr + AddVector(const common::Coordinate& center, + const std::shared_ptr& + vectorPacket); /** * Sets the modulate color of a linked vector. diff --git a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp index 42068b36..6cc6fbb6 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp @@ -2,12 +2,13 @@ #include #include #include -#include #include #include #include #include +#include #include +#include namespace scwx { @@ -33,20 +34,33 @@ class OverlayProductLayer::Impl void UpdateStormTrackingInformation(); static void HandleLinkedVectorPacket( - const std::shared_ptr& packet, - const common::Coordinate& center, - const std::string& hoverText, - boost::gil::rgba32f_pixel_t color, - bool tickRadiusIncrement, - std::shared_ptr& linkedVectors); + const std::shared_ptr& packet, + const common::Coordinate& center, + const std::string& hoverText, + boost::gil::rgba32f_pixel_t color, + units::length::nautical_miles tickRadius, + units::length::nautical_miles tickRadiusIncrement, + std::shared_ptr& linkedVectors); static void HandleScitDataPacket( - const std::shared_ptr& packet, - const common::Coordinate& center, - const std::string& stormId, - std::shared_ptr& linkedVectors); - static void - HandleStormIdPacket(const std::shared_ptr& packet, - std::string& stormId); + const std::shared_ptr& + sti, + const std::shared_ptr& packet, + const common::Coordinate& center, + const std::string& stormId, + const std::string& hoverText, + std::shared_ptr& linkedVectors); + + static void HandleStormIdPacket( + const std::shared_ptr& + sti, + const std::shared_ptr& packet, + std::string& stormId, + std::string& hoverText); + + static std::string BuildHoverText( + const std::shared_ptr< + const scwx::wsr88d::rpg::StormTrackingInformationMessage>& sti, + std::string& stormId); OverlayProductLayer* self_; @@ -122,21 +136,21 @@ void OverlayProductLayer::Impl::UpdateStormTrackingInformation() float latitude = 0.0f; float longitude = 0.0f; - std::shared_ptr l3File = nullptr; - std::shared_ptr gpm = nullptr; - std::shared_ptr psb = nullptr; + std::shared_ptr l3File = nullptr; + std::shared_ptr sti = nullptr; + std::shared_ptr psb = nullptr; if (record != nullptr) { l3File = record->level3_file(); } if (l3File != nullptr) { - gpm = std::dynamic_pointer_cast( - l3File->message()); + sti = std::dynamic_pointer_cast< + wsr88d::rpg::StormTrackingInformationMessage>(l3File->message()); } - if (gpm != nullptr) + if (sti != nullptr) { - psb = gpm->symbology_block(); + psb = sti->symbology_block(); } linkedVectors_->StartVectors(); @@ -155,6 +169,7 @@ void OverlayProductLayer::Impl::UpdateStormTrackingInformation() } std::string stormId = "?"; + std::string hoverText {}; for (std::size_t i = 0; i < psb->number_of_layers(); ++i) { @@ -164,15 +179,19 @@ void OverlayProductLayer::Impl::UpdateStormTrackingInformation() switch (packet->packet_code()) { case static_cast(wsr88d::rpg::PacketCode::StormId): - HandleStormIdPacket(packet, stormId); + HandleStormIdPacket(sti, packet, stormId, hoverText); break; case static_cast( wsr88d::rpg::PacketCode::ScitPastData): case static_cast( wsr88d::rpg::PacketCode::ScitForecastData): - HandleScitDataPacket( - packet, {latitude, longitude}, stormId, linkedVectors_); + HandleScitDataPacket(sti, + packet, + {latitude, longitude}, + stormId, + hoverText, + linkedVectors_); break; default: @@ -192,41 +211,63 @@ void OverlayProductLayer::Impl::UpdateStormTrackingInformation() } void OverlayProductLayer::Impl::HandleStormIdPacket( - const std::shared_ptr& packet, std::string& stormId) + const std::shared_ptr& + sti, + const std::shared_ptr& packet, + std::string& stormId, + std::string& hoverText) { auto stormIdPacket = - std::dynamic_pointer_cast(packet); + std::dynamic_pointer_cast(packet); if (stormIdPacket != nullptr && stormIdPacket->RecordCount() > 0) { - stormId = stormIdPacket->storm_id(0); + stormId = stormIdPacket->storm_id(0); + hoverText = BuildHoverText(sti, stormId); } else { logger_->warn("Invalid Storm ID Packet"); stormId = "?"; + hoverText.clear(); } } void OverlayProductLayer::Impl::HandleScitDataPacket( - const std::shared_ptr& packet, - const common::Coordinate& center, - const std::string& stormId, - std::shared_ptr& linkedVectors) + const std::shared_ptr& + sti, + const std::shared_ptr& packet, + const common::Coordinate& center, + const std::string& stormId, + const std::string& hoverText, + std::shared_ptr& linkedVectors) { auto scitDataPacket = - std::dynamic_pointer_cast(packet); + std::dynamic_pointer_cast(packet); if (scitDataPacket != nullptr) { boost::gil::rgba32f_pixel_t color {1.0f, 1.0f, 1.0f, 1.0f}; - bool tickRadiusIncrement = true; + + units::length::nautical_miles tickRadius {0.5f}; + units::length::nautical_miles tickRadiusIncrement {0.0f}; + + auto stiRecord = sti->sti_record(stormId); + if (scitDataPacket->packet_code() == static_cast(wsr88d::rpg::PacketCode::ScitPastData)) { - color = {0.5f, 0.5f, 0.5f, 1.0f}; - tickRadiusIncrement = false; + // If this is past data, the default tick radius and increment with a + // darker color + color = {0.5f, 0.5f, 0.5f, 1.0f}; + } + else if (stiRecord != nullptr && stiRecord->meanError_.has_value()) + { + // If this is forecast data, use the mean error as the radius (minimum + // of the default value), incrementing by the mean error + tickRadiusIncrement = stiRecord->meanError_.value(); + tickRadius = std::max(tickRadius, tickRadiusIncrement); } for (auto& subpacket : scitDataPacket->packet_list()) @@ -237,8 +278,9 @@ void OverlayProductLayer::Impl::HandleScitDataPacket( wsr88d::rpg::PacketCode::LinkedVectorNoValue): HandleLinkedVectorPacket(subpacket, center, - stormId, + hoverText, color, + tickRadius, tickRadiusIncrement, linkedVectors); break; @@ -257,15 +299,16 @@ void OverlayProductLayer::Impl::HandleScitDataPacket( } void OverlayProductLayer::Impl::HandleLinkedVectorPacket( - const std::shared_ptr& packet, - const common::Coordinate& center, - const std::string& hoverText, - boost::gil::rgba32f_pixel_t color, - bool tickRadiusIncrement, - std::shared_ptr& linkedVectors) + const std::shared_ptr& packet, + const common::Coordinate& center, + const std::string& hoverText, + boost::gil::rgba32f_pixel_t color, + units::length::nautical_miles tickRadius, + units::length::nautical_miles tickRadiusIncrement, + std::shared_ptr& linkedVectors) { auto linkedVectorPacket = - std::dynamic_pointer_cast(packet); + std::dynamic_pointer_cast(packet); if (linkedVectorPacket != nullptr) { @@ -274,24 +317,74 @@ void OverlayProductLayer::Impl::HandleLinkedVectorPacket( gl::draw::LinkedVectors::SetVectorModulate(di, color); gl::draw::LinkedVectors::SetVectorHoverText(di, hoverText); gl::draw::LinkedVectors::SetVectorTicksEnabled(di, true); - gl::draw::LinkedVectors::SetVectorTickRadius( - di, units::length::nautical_miles {1.0}); + gl::draw::LinkedVectors::SetVectorTickRadius(di, tickRadius); + gl::draw::LinkedVectors::SetVectorTickRadiusIncrement( + di, tickRadiusIncrement); + } + else + { + logger_->warn("Invalid Linked Vector Packet"); + } +} - if (tickRadiusIncrement) +std::string OverlayProductLayer::Impl::BuildHoverText( + const std::shared_ptr< + const scwx::wsr88d::rpg::StormTrackingInformationMessage>& sti, + std::string& stormId) +{ + std::string hoverText = fmt::format("Storm ID: {}", stormId); + + auto stiRecord = sti->sti_record(stormId); + + if (stiRecord != nullptr) + { + if (stiRecord->direction_.has_value() && stiRecord->speed_.has_value()) { - gl::draw::LinkedVectors::SetVectorTickRadiusIncrement( - di, units::length::nautical_miles {1.0}); + hoverText += + fmt::format("\nMovement: {} @ {}", + units::to_string(stiRecord->direction_.value()), + units::to_string(stiRecord->speed_.value())); } - else + + if (stiRecord->maxDbz_.has_value() && + stiRecord->maxDbzHeight_.has_value()) + { + hoverText += + fmt::format("\nMax dBZ: {} ({} kft)", + stiRecord->maxDbz_.value(), + stiRecord->maxDbzHeight_.value().value() / 1000.0f); + } + + if (stiRecord->forecastError_.has_value()) + { + hoverText += + fmt::format("\nForecast Error: {}", + units::to_string(stiRecord->forecastError_.value())); + } + + if (stiRecord->meanError_.has_value()) { - gl::draw::LinkedVectors::SetVectorTickRadiusIncrement( - di, units::length::nautical_miles {0.0}); + hoverText += + fmt::format("\nMean Error: {}", + units::to_string(stiRecord->meanError_.value())); } } - else + + auto dateTime = sti->date_time(); + if (dateTime.has_value()) { - logger_->warn("Invalid Linked Vector Packet"); + hoverText += + fmt::format("\nDate/Time: {}", util::TimeString(dateTime.value())); } + + auto forecastInterval = sti->forecast_interval(); + if (forecastInterval.has_value()) + { + hoverText += fmt::format("\nForecast Interval: {} min", + forecastInterval.value().count()); + } + + return hoverText; } bool OverlayProductLayer::RunMousePicking( From 5b9df2b61a75297d2517c233b48e1a81c79720d3 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 24 Feb 2024 22:50:12 -0600 Subject: [PATCH 34/37] Fix loading and updating of NST data, consistent with other level 3 data --- .../scwx/qt/map/overlay_product_layer.cpp | 11 +- .../scwx/qt/view/overlay_product_view.cpp | 110 +++++++++++++----- .../scwx/qt/view/overlay_product_view.hpp | 16 ++- .../storm_tracking_information_message.cpp | 2 +- 4 files changed, 98 insertions(+), 41 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp index 6cc6fbb6..da580228 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp @@ -131,22 +131,17 @@ void OverlayProductLayer::Impl::UpdateStormTrackingInformation() auto overlayProductView = self_->context()->overlay_product_view(); auto radarProductManager = overlayProductView->radar_product_manager(); - auto record = overlayProductView->radar_product_record("NST"); + auto message = overlayProductView->radar_product_message("NST"); float latitude = 0.0f; float longitude = 0.0f; - std::shared_ptr l3File = nullptr; std::shared_ptr sti = nullptr; std::shared_ptr psb = nullptr; - if (record != nullptr) - { - l3File = record->level3_file(); - } - if (l3File != nullptr) + if (message != nullptr) { sti = std::dynamic_pointer_cast< - wsr88d::rpg::StormTrackingInformationMessage>(l3File->message()); + wsr88d::rpg::StormTrackingInformationMessage>(message); } if (sti != nullptr) { diff --git a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp index c57e7010..2ad6fa22 100644 --- a/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp +++ b/scwx-qt/source/scwx/qt/view/overlay_product_view.cpp @@ -32,6 +32,7 @@ class OverlayProductView::Impl std::chrono::system_clock::time_point time, bool autoUpdate); void ResetProducts(); + void Update(const std::string& product); void UpdateAutoRefresh(bool enabled) const; OverlayProductView* self_; @@ -46,9 +47,9 @@ class OverlayProductView::Impl std::shared_ptr radarProductManager_ {nullptr}; - std::unordered_map> - recordMap_ {}; - std::mutex recordMutex_ {}; + std::unordered_map> + messageMap_ {}; + std::mutex messageMutex_ {}; }; OverlayProductView::OverlayProductView() : p(std::make_unique(this)) {}; @@ -60,13 +61,13 @@ OverlayProductView::radar_product_manager() const return p->radarProductManager_; } -std::shared_ptr -OverlayProductView::radar_product_record(const std::string& product) const +std::shared_ptr +OverlayProductView::radar_product_message(const std::string& product) const { - std::unique_lock lock {p->recordMutex_}; + std::unique_lock lock {p->messageMutex_}; - auto it = p->recordMap_.find(product); - if (it != p->recordMap_.cend()) + auto it = p->messageMap_.find(product); + if (it != p->messageMap_.cend()) { return it->second; } @@ -94,6 +95,23 @@ void OverlayProductView::set_radar_product_manager( void OverlayProductView::Impl::ConnectRadarProductManager() { + connect(radarProductManager_.get(), + &manager::RadarProductManager::DataReloaded, + self_, + [this](std::shared_ptr record) + { + if (record->radar_product_group() == + common::RadarProductGroup::Level3 && + record->radar_product() == kNst_ && + std::chrono::floor(record->time()) == + selectedTime_) + { + // If the data associated with the currently selected time is + // reloaded, update the view + Update(record->radar_product()); + } + }); + connect( radarProductManager_.get(), &manager::RadarProductManager::NewDataAvailable, @@ -148,17 +166,24 @@ void OverlayProductView::Impl::LoadProduct( // Select loaded record const auto& record = request->radar_product_record(); - // Validate record + std::shared_ptr level3File = nullptr; + std::shared_ptr message = nullptr; + if (record != nullptr) { - auto productTime = record->time(); - auto level3File = record->level3_file(); - if (level3File != nullptr) - { - const auto& header = level3File->message()->header(); - productTime = util::TimePoint( - header.date_of_message(), header.time_of_message() * 1000); - } + level3File = record->level3_file(); + } + if (level3File != nullptr) + { + message = level3File->message(); + } + + // Validate record + if (message != nullptr) + { + const auto& header = message->header(); + auto productTime = util::TimePoint( + header.date_of_message(), header.time_of_message() * 1000); // If the record is from the last 30 minutes if (productTime + 30min >= std::chrono::system_clock::now() || @@ -166,12 +191,12 @@ void OverlayProductView::Impl::LoadProduct( productTime + 30min >= selectedTime_)) { // Store loaded record - std::unique_lock lock {recordMutex_}; + std::unique_lock lock {messageMutex_}; - auto it = recordMap_.find(product); - if (it == recordMap_.cend() || it->second != record) + auto it = messageMap_.find(product); + if (it == messageMap_.cend() || it->second != message) { - recordMap_.insert_or_assign(product, record); + messageMap_.insert_or_assign(product, message); lock.unlock(); @@ -181,8 +206,8 @@ void OverlayProductView::Impl::LoadProduct( else { // If product is more than 30 minutes old, discard - std::unique_lock lock {recordMutex_}; - std::size_t elementsRemoved = recordMap_.erase(product); + std::unique_lock lock {messageMutex_}; + std::size_t elementsRemoved = messageMap_.erase(product); lock.unlock(); if (elementsRemoved > 0) @@ -197,8 +222,8 @@ void OverlayProductView::Impl::LoadProduct( else { // If the product doesn't exist, erase the stale product - std::unique_lock lock {recordMutex_}; - std::size_t elementsRemoved = recordMap_.erase(product); + std::unique_lock lock {messageMutex_}; + std::size_t elementsRemoved = messageMap_.erase(product); lock.unlock(); if (elementsRemoved > 0) @@ -220,8 +245,8 @@ void OverlayProductView::Impl::LoadProduct( void OverlayProductView::Impl::ResetProducts() { - std::unique_lock lock {recordMutex_}; - recordMap_.clear(); + std::unique_lock lock {messageMutex_}; + messageMap_.clear(); lock.unlock(); Q_EMIT self_->ProductUpdated(kNst_); @@ -232,7 +257,7 @@ void OverlayProductView::SelectTime(std::chrono::system_clock::time_point time) if (time != p->selectedTime_) { p->selectedTime_ = time; - p->LoadProduct(kNst_, time, true); + p->Update(kNst_); } } @@ -245,6 +270,35 @@ void OverlayProductView::SetAutoRefresh(bool enabled) } } +void OverlayProductView::Impl::Update(const std::string& product) +{ + // Retrieve message from Radar Product Manager + std::shared_ptr message; + std::chrono::system_clock::time_point requestedTime {selectedTime_}; + std::chrono::system_clock::time_point foundTime; + std::tie(message, foundTime) = + radarProductManager_->GetLevel3Data(product, requestedTime); + + if (message == nullptr) + { + logger_->debug("{} data not found", product); + return; + } + + std::unique_lock lock {messageMutex_}; + + // Update message in map + auto it = messageMap_.find(product); + if (it == messageMap_.cend() || it->second != message) + { + messageMap_.insert_or_assign(product, message); + + lock.unlock(); + + Q_EMIT self_->ProductUpdated(product); + } +} + void OverlayProductView::Impl::UpdateAutoRefresh(bool enabled) const { if (radarProductManager_ != nullptr) diff --git a/scwx-qt/source/scwx/qt/view/overlay_product_view.hpp b/scwx-qt/source/scwx/qt/view/overlay_product_view.hpp index 31e1cec0..e0083aaf 100644 --- a/scwx-qt/source/scwx/qt/view/overlay_product_view.hpp +++ b/scwx-qt/source/scwx/qt/view/overlay_product_view.hpp @@ -1,7 +1,5 @@ #pragma once -#include - #include #include @@ -9,6 +7,16 @@ namespace scwx { +namespace wsr88d +{ +namespace rpg +{ + +class Level3Message; + +} // namespace rpg +} // namespace wsr88d + namespace qt { namespace manager @@ -30,8 +38,8 @@ class OverlayProductView : public QObject virtual ~OverlayProductView(); std::shared_ptr radar_product_manager() const; - std::shared_ptr - radar_product_record(const std::string& product) const; + std::shared_ptr + radar_product_message(const std::string& product) const; std::chrono::system_clock::time_point selected_time() const; void set_radar_product_manager( diff --git a/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp b/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp index b61b05c8..1e920f4b 100644 --- a/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp +++ b/wxdata/source/scwx/wsr88d/rpg/storm_tracking_information_message.cpp @@ -433,7 +433,7 @@ void StormTrackingInformationMessage::Impl::HandleTextUniformPacket( } else { - logger_->warn("Invalid Text Uniform Packet"); + logger_->debug("No storm text present"); } } From 5e0f69018bdc1292ed69acc43d3aa32e8b805418 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 24 Feb 2024 23:35:59 -0600 Subject: [PATCH 35/37] Add storm tracking information to settings --- scwx-qt/scwx-qt.cmake | 2 + .../scwx/qt/manager/settings_manager.cpp | 5 ++ .../scwx/qt/settings/product_settings.cpp | 75 +++++++++++++++++++ .../scwx/qt/settings/product_settings.hpp | 45 +++++++++++ test/data | 2 +- 5 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 scwx-qt/source/scwx/qt/settings/product_settings.cpp create mode 100644 scwx-qt/source/scwx/qt/settings/product_settings.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index ec0979d1..7fcbed33 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -158,6 +158,7 @@ set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp source/scwx/qt/settings/general_settings.hpp source/scwx/qt/settings/map_settings.hpp source/scwx/qt/settings/palette_settings.hpp + source/scwx/qt/settings/product_settings.hpp source/scwx/qt/settings/settings_category.hpp source/scwx/qt/settings/settings_container.hpp source/scwx/qt/settings/settings_definitions.hpp @@ -171,6 +172,7 @@ set(SRC_SETTINGS source/scwx/qt/settings/audio_settings.cpp source/scwx/qt/settings/general_settings.cpp source/scwx/qt/settings/map_settings.cpp source/scwx/qt/settings/palette_settings.cpp + source/scwx/qt/settings/product_settings.cpp source/scwx/qt/settings/settings_category.cpp source/scwx/qt/settings/settings_container.cpp source/scwx/qt/settings/settings_interface.cpp diff --git a/scwx-qt/source/scwx/qt/manager/settings_manager.cpp b/scwx-qt/source/scwx/qt/manager/settings_manager.cpp index 36ad057c..8a244054 100644 --- a/scwx-qt/source/scwx/qt/manager/settings_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/settings_manager.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -116,6 +117,7 @@ void SettingsManager::Shutdown() dataChanged |= settings::GeneralSettings::Instance().Shutdown(); dataChanged |= settings::MapSettings::Instance().Shutdown(); + dataChanged |= settings::ProductSettings::Instance().Shutdown(); dataChanged |= settings::UiSettings::Instance().Shutdown(); if (dataChanged) @@ -132,6 +134,7 @@ boost::json::value SettingsManager::Impl::ConvertSettingsToJson() settings::AudioSettings::Instance().WriteJson(settingsJson); settings::MapSettings::Instance().WriteJson(settingsJson); settings::PaletteSettings::Instance().WriteJson(settingsJson); + settings::ProductSettings::Instance().WriteJson(settingsJson); settings::TextSettings::Instance().WriteJson(settingsJson); settings::UiSettings::Instance().WriteJson(settingsJson); @@ -146,6 +149,7 @@ void SettingsManager::Impl::GenerateDefaultSettings() settings::AudioSettings::Instance().SetDefaults(); settings::MapSettings::Instance().SetDefaults(); settings::PaletteSettings::Instance().SetDefaults(); + settings::ProductSettings::Instance().SetDefaults(); settings::TextSettings::Instance().SetDefaults(); settings::UiSettings::Instance().SetDefaults(); } @@ -161,6 +165,7 @@ bool SettingsManager::Impl::LoadSettings( jsonDirty |= !settings::AudioSettings::Instance().ReadJson(settingsJson); jsonDirty |= !settings::MapSettings::Instance().ReadJson(settingsJson); jsonDirty |= !settings::PaletteSettings::Instance().ReadJson(settingsJson); + jsonDirty |= !settings::ProductSettings::Instance().ReadJson(settingsJson); jsonDirty |= !settings::TextSettings::Instance().ReadJson(settingsJson); jsonDirty |= !settings::UiSettings::Instance().ReadJson(settingsJson); diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.cpp b/scwx-qt/source/scwx/qt/settings/product_settings.cpp new file mode 100644 index 00000000..3cf47ef7 --- /dev/null +++ b/scwx-qt/source/scwx/qt/settings/product_settings.cpp @@ -0,0 +1,75 @@ +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace settings +{ + +static const std::string logPrefix_ = "scwx::qt::settings::product_settings"; + +class ProductSettings::Impl +{ +public: + explicit Impl() + { + stiForecastEnabled_.SetDefault(true); + stiPastEnabled_.SetDefault(true); + } + + ~Impl() {} + + SettingsVariable stiForecastEnabled_ {"sti_forecast_enabled"}; + SettingsVariable stiPastEnabled_ {"sti_past_enabled"}; +}; + +ProductSettings::ProductSettings() : + SettingsCategory("product"), p(std::make_unique()) +{ + RegisterVariables({&p->stiForecastEnabled_, &p->stiPastEnabled_}); + SetDefaults(); +} +ProductSettings::~ProductSettings() = default; + +ProductSettings::ProductSettings(ProductSettings&&) noexcept = default; +ProductSettings& +ProductSettings::operator=(ProductSettings&&) noexcept = default; + +SettingsVariable& ProductSettings::sti_forecast_enabled() const +{ + return p->stiForecastEnabled_; +} + +SettingsVariable& ProductSettings::sti_past_enabled() const +{ + return p->stiPastEnabled_; +} + +bool ProductSettings::Shutdown() +{ + bool dataChanged = false; + + // Commit settings that are managed separate from the settings dialog + dataChanged |= p->stiForecastEnabled_.Commit(); + dataChanged |= p->stiPastEnabled_.Commit(); + + return dataChanged; +} + +ProductSettings& ProductSettings::Instance() +{ + static ProductSettings generalSettings_; + return generalSettings_; +} + +bool operator==(const ProductSettings& lhs, const ProductSettings& rhs) +{ + return (lhs.p->stiForecastEnabled_ == rhs.p->stiForecastEnabled_ && + lhs.p->stiPastEnabled_ == rhs.p->stiPastEnabled_); +} + +} // namespace settings +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/settings/product_settings.hpp b/scwx-qt/source/scwx/qt/settings/product_settings.hpp new file mode 100644 index 00000000..c7c09dd8 --- /dev/null +++ b/scwx-qt/source/scwx/qt/settings/product_settings.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace settings +{ + +class ProductSettings : public SettingsCategory +{ +public: + explicit ProductSettings(); + ~ProductSettings(); + + ProductSettings(const ProductSettings&) = delete; + ProductSettings& operator=(const ProductSettings&) = delete; + + ProductSettings(ProductSettings&&) noexcept; + ProductSettings& operator=(ProductSettings&&) noexcept; + + SettingsVariable& sti_forecast_enabled() const; + SettingsVariable& sti_past_enabled() const; + + static ProductSettings& Instance(); + + friend bool operator==(const ProductSettings& lhs, + const ProductSettings& rhs); + + bool Shutdown(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace settings +} // namespace qt +} // namespace scwx diff --git a/test/data b/test/data index 90331f76..0446ff70 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 90331f7654586302b223d88329846929514bc883 +Subproject commit 0446ff708b387728faf8d2dbb3862a757067c2ee From 5f191a84682172497b20a8d8d37ba674e4241728 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 25 Feb 2024 00:15:53 -0600 Subject: [PATCH 36/37] Add storm track settings to level 3 products widget --- .../scwx/qt/ui/level3_products_widget.cpp | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp index 40ba6c56..99f22092 100644 --- a/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/level3_products_widget.cpp @@ -1,12 +1,16 @@ #include #include +#include +#include #include #include #include +#include #include #include +#include namespace scwx { @@ -25,13 +29,17 @@ class Level3ProductsWidgetImpl : public QObject public: explicit Level3ProductsWidgetImpl(Level3ProductsWidget* self) : self_ {self}, - layout_ {new ui::FlowLayout(self)}, + layout_ {new QVBoxLayout(self)}, + productsWidget_ {new QWidget(self)}, + productsLayout_ {new ui::FlowLayout(productsWidget_)}, categoryButtons_ {}, productTiltMap_ {}, awipsProductMap_ {}, awipsProductMutex_ {} { layout_->setContentsMargins(0, 0, 0, 0); + layout_->addWidget(productsWidget_); + productsLayout_->setContentsMargins(0, 0, 0, 0); for (common::Level3ProductCategory category : common::Level3ProductCategoryIterator()) @@ -42,7 +50,7 @@ class Level3ProductsWidgetImpl : public QObject toolButton->setStatusTip( tr(common::GetLevel3CategoryDescription(category).c_str())); toolButton->setPopupMode(QToolButton::MenuButtonPopup); - layout_->addWidget(toolButton); + productsLayout_->addWidget(toolButton); categoryButtons_.push_back(toolButton); QObject::connect(toolButton, @@ -99,6 +107,26 @@ class Level3ProductsWidgetImpl : public QObject toolButton->setEnabled(false); } + + // Storm Tracking Information + QCheckBox* stiPastEnableCheckBox = new QCheckBox(); + QCheckBox* stiForecastEnableCheckBox = new QCheckBox(); + + stiPastEnableCheckBox->setText(QObject::tr("Storm Tracks (Past)")); + stiForecastEnableCheckBox->setText( + QObject::tr("Storm Tracks (Forecast)")); + + layout_->addWidget(stiPastEnableCheckBox); + layout_->addWidget(stiForecastEnableCheckBox); + + auto& productSettings = settings::ProductSettings::Instance(); + + stiPastEnabled_.SetSettingsVariable(productSettings.sti_past_enabled()); + stiForecastEnabled_.SetSettingsVariable( + productSettings.sti_forecast_enabled()); + + stiPastEnabled_.SetEditWidget(stiPastEnableCheckBox); + stiForecastEnabled_.SetEditWidget(stiForecastEnableCheckBox); } ~Level3ProductsWidgetImpl() = default; @@ -109,6 +137,8 @@ class Level3ProductsWidgetImpl : public QObject Level3ProductsWidget* self_; QLayout* layout_; + QWidget* productsWidget_; + QLayout* productsLayout_; std::list categoryButtons_; std::unordered_map> @@ -118,6 +148,9 @@ class Level3ProductsWidgetImpl : public QObject std::unordered_map awipsProductMap_; std::shared_mutex awipsProductMutex_; + + settings::SettingsInterface stiPastEnabled_ {}; + settings::SettingsInterface stiForecastEnabled_ {}; }; Level3ProductsWidget::Level3ProductsWidget(QWidget* parent) : From 80590296a340e9b89dc078c3e5c23b89a559e17f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 25 Feb 2024 00:41:03 -0600 Subject: [PATCH 37/37] Update storm track display based upon settings --- .../scwx/qt/map/overlay_product_layer.cpp | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp index da580228..d7e19b2f 100644 --- a/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp +++ b/scwx-qt/source/scwx/qt/map/overlay_product_layer.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,28 @@ class OverlayProductLayer::Impl self_ {self}, linkedVectors_ {std::make_shared(context)} { + auto& productSettings = settings::ProductSettings::Instance(); + + productSettings.sti_forecast_enabled().RegisterValueStagedCallback( + [=, this](const bool& value) + { + stiForecastEnabled_ = value; + stiNeedsUpdate_ = true; + + Q_EMIT self_->NeedsRendering(); + }); + productSettings.sti_past_enabled().RegisterValueStagedCallback( + [=, this](const bool& value) + { + stiPastEnabled_ = value; + stiNeedsUpdate_ = true; + + Q_EMIT self_->NeedsRendering(); + }); + + stiForecastEnabled_ = + productSettings.sti_forecast_enabled().GetStagedOrValue(); + stiPastEnabled_ = productSettings.sti_past_enabled().GetStagedOrValue(); } ~Impl() = default; @@ -41,7 +64,7 @@ class OverlayProductLayer::Impl units::length::nautical_miles tickRadius, units::length::nautical_miles tickRadiusIncrement, std::shared_ptr& linkedVectors); - static void HandleScitDataPacket( + void HandleScitDataPacket( const std::shared_ptr& sti, const std::shared_ptr& packet, @@ -64,6 +87,9 @@ class OverlayProductLayer::Impl OverlayProductLayer* self_; + bool stiForecastEnabled_ {true}; + bool stiPastEnabled_ {true}; + bool stiNeedsUpdate_ {false}; std::shared_ptr linkedVectors_; @@ -81,6 +107,7 @@ OverlayProductLayer::OverlayProductLayer(std::shared_ptr context) : if (product == "NST") { p->stiNeedsUpdate_ = true; + Q_EMIT NeedsRendering(); } }); @@ -253,16 +280,29 @@ void OverlayProductLayer::Impl::HandleScitDataPacket( if (scitDataPacket->packet_code() == static_cast(wsr88d::rpg::PacketCode::ScitPastData)) { + if (!stiPastEnabled_) + { + return; + } + // If this is past data, the default tick radius and increment with a // darker color color = {0.5f, 0.5f, 0.5f, 1.0f}; } - else if (stiRecord != nullptr && stiRecord->meanError_.has_value()) + else { - // If this is forecast data, use the mean error as the radius (minimum - // of the default value), incrementing by the mean error - tickRadiusIncrement = stiRecord->meanError_.value(); - tickRadius = std::max(tickRadius, tickRadiusIncrement); + if (!stiForecastEnabled_) + { + return; + } + + if (stiRecord != nullptr && stiRecord->meanError_.has_value()) + { + // If this is forecast data, use the mean error as the radius + // (minimum of the default value), incrementing by the mean error + tickRadiusIncrement = stiRecord->meanError_.value(); + tickRadius = std::max(tickRadius, tickRadiusIncrement); + } } for (auto& subpacket : scitDataPacket->packet_list())