diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ca50212..4f87176c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ jobs: env: CC: ${{ matrix.env_cc }} CXX: ${{ matrix.env_cxx }} - SCWX_VERSION: v0.4.4 + SCWX_VERSION: v0.4.5 runs-on: ${{ matrix.os }} steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index 08b5e823..c06a46e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.21) set(PROJECT_NAME supercell-wx) project(${PROJECT_NAME} - VERSION 0.4.3 + VERSION 0.4.5 DESCRIPTION "Supercell Wx is a free, open source advanced weather radar viewer." HOMEPAGE_URL "https://github.com/dpaulat/supercell-wx" LANGUAGES C CXX) @@ -44,7 +44,7 @@ conan_basic_setup(TARGETS) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DBOOST_ALL_NO_LIB") set(SCWX_DIR ${PROJECT_SOURCE_DIR}) -set(SCWX_VERSION "0.4.4") +set(SCWX_VERSION "0.4.5") option(SCWX_ADDRESS_SANITIZER "Build with Address Sanitizer" OFF) diff --git a/scwx-qt/res/scwx-qt.rc b/scwx-qt/res/scwx-qt.rc index 747c4319..9bb51c0d 100644 --- a/scwx-qt/res/scwx-qt.rc +++ b/scwx-qt/res/scwx-qt.rc @@ -3,8 +3,8 @@ IDI_ICON1 ICON "icons\\scwx-256.ico" VS_VERSION_INFO VERSIONINFO - FILEVERSION 0,4,4,0 - PRODUCTVERSION 0,4,4,0 + FILEVERSION 0,4,5,0 + PRODUCTVERSION 0,4,5,0 FILEFLAGS 0x0L FILEFLAGSMASK 0x3fL FILEOS 0x00040004L @@ -17,12 +17,12 @@ BEGIN BEGIN VALUE "CompanyName", "Dan Paulat" VALUE "FileDescription", "Supercell Wx" - VALUE "FileVersion", "0.4.4.0" + VALUE "FileVersion", "0.4.5.0" VALUE "LegalCopyright", "Copyright (C) 2021-2024 Dan Paulat" VALUE "InternalName", "scwx" VALUE "OriginalFilename", "supercell-wx.exe" VALUE "ProductName", "Supercell Wx" - VALUE "ProductVersion", "0.4.4.0" + VALUE "ProductVersion", "0.4.5.0" END END BLOCK "VarFileInfo" diff --git a/scwx-qt/source/scwx/qt/model/alert_model.cpp b/scwx-qt/source/scwx/qt/model/alert_model.cpp index 4cadff42..76cd5a5c 100644 --- a/scwx-qt/source/scwx/qt/model/alert_model.cpp +++ b/scwx-qt/source/scwx/qt/model/alert_model.cpp @@ -34,6 +34,10 @@ public: explicit AlertModelImpl(); ~AlertModelImpl() = default; + bool GetObserved(const types::TextEventKey& key); + awips::ThreatCategory GetThreatCategory(const types::TextEventKey& key); + bool GetTornadoPossible(const types::TextEventKey& key); + static std::string GetCounties(const types::TextEventKey& key); static std::string GetState(const types::TextEventKey& key); static std::chrono::system_clock::time_point @@ -49,6 +53,18 @@ public: const GeographicLib::Geodesic& geodesic_; + std::unordered_map> + observedMap_; + std::unordered_map> + threatCategoryMap_; + std::unordered_map> + tornadoPossibleMap_; std::unordered_map> @@ -125,6 +141,29 @@ QVariant AlertModel::data(const QModelIndex& index, int role) const return QString::fromStdString( awips::GetSignificanceText(textEventKey.significance_)); + case static_cast(Column::Tornado): + if (textEventKey.phenomenon_ == awips::Phenomenon::Tornado && + p->GetObserved(textEventKey)) + { + return tr("Observed"); + } + if (p->GetTornadoPossible(textEventKey)) + { + return tr("Possible"); + } + break; + + case static_cast(Column::ThreatCategory): + if (role == Qt::DisplayRole) + { + return QString::fromStdString(awips::GetThreatCategoryName( + p->GetThreatCategory(textEventKey))); + } + else + { + return static_cast(p->GetThreatCategory(textEventKey)); + } + case static_cast(Column::State): return QString::fromStdString(AlertModelImpl::GetState(textEventKey)); @@ -204,6 +243,12 @@ AlertModel::headerData(int section, Qt::Orientation orientation, int role) const case static_cast(Column::Significance): return tr("Significance"); + case static_cast(Column::ThreatCategory): + return tr("Category"); + + case static_cast(Column::Tornado): + return tr("Tornado"); + case static_cast(Column::State): return tr("State"); @@ -240,6 +285,14 @@ AlertModel::headerData(int section, Qt::Orientation orientation, int role) const contentsSize = fontMetrics.size(0, QString(10, 'W')); break; + case static_cast(Column::ThreatCategory): + contentsSize = fontMetrics.size(0, QString(6, 'W')); + break; + + case static_cast(Column::Tornado): + contentsSize = fontMetrics.size(0, QString(5, 'W')); + break; + case static_cast(Column::State): contentsSize = fontMetrics.size(0, "WW, WW"); break; @@ -285,6 +338,12 @@ void AlertModel::HandleAlert(const types::TextEventKey& alertKey, std::shared_ptr alertSegment = alertMessages[messageIndex]->segments().back(); + p->observedMap_.insert_or_assign(alertKey, alertSegment->observed_); + p->threatCategoryMap_.insert_or_assign(alertKey, + alertSegment->threatCategory_); + p->tornadoPossibleMap_.insert_or_assign(alertKey, + alertSegment->tornadoPossible_); + if (alertSegment->codedLocation_.has_value()) { // Update centroid and distance @@ -365,6 +424,46 @@ AlertModelImpl::AlertModelImpl() : { } +bool AlertModelImpl::GetObserved(const types::TextEventKey& key) +{ + bool observed = false; + + auto it = observedMap_.find(key); + if (it != observedMap_.cend()) + { + observed = it->second; + } + + return observed; +} + +awips::ThreatCategory +AlertModelImpl::GetThreatCategory(const types::TextEventKey& key) +{ + awips::ThreatCategory threatCategory = awips::ThreatCategory::Base; + + auto it = threatCategoryMap_.find(key); + if (it != threatCategoryMap_.cend()) + { + threatCategory = it->second; + } + + return threatCategory; +} + +bool AlertModelImpl::GetTornadoPossible(const types::TextEventKey& key) +{ + bool tornadoPossible = false; + + auto it = tornadoPossibleMap_.find(key); + if (it != tornadoPossibleMap_.cend()) + { + tornadoPossible = it->second; + } + + return tornadoPossible; +} + std::string AlertModelImpl::GetCounties(const types::TextEventKey& key) { auto messageList = manager::TextEventManager::Instance()->message_list(key); diff --git a/scwx-qt/source/scwx/qt/model/alert_model.hpp b/scwx-qt/source/scwx/qt/model/alert_model.hpp index 76bddf9d..df6d561e 100644 --- a/scwx-qt/source/scwx/qt/model/alert_model.hpp +++ b/scwx-qt/source/scwx/qt/model/alert_model.hpp @@ -21,15 +21,17 @@ class AlertModel : public QAbstractTableModel public: enum class Column : int { - Etn = 0, - OfficeId = 1, - Phenomenon = 2, - Significance = 3, - State = 4, - Counties = 5, - StartTime = 6, - EndTime = 7, - Distance = 8 + Etn = 0, + OfficeId = 1, + Phenomenon = 2, + Significance = 3, + ThreatCategory = 4, + Tornado = 5, + State = 6, + Counties = 7, + StartTime = 8, + EndTime = 9, + Distance = 10 }; explicit AlertModel(QObject* parent = nullptr); diff --git a/wxdata/include/scwx/awips/impact_based_warnings.hpp b/wxdata/include/scwx/awips/impact_based_warnings.hpp new file mode 100644 index 00000000..a7b22288 --- /dev/null +++ b/wxdata/include/scwx/awips/impact_based_warnings.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace scwx +{ +namespace awips +{ + +enum class ThreatCategory : int +{ + Base = 0, + Significant = 1, + Considerable = 2, + Destructive = 3, + Catastrophic = 4, + Unknown +}; + +ThreatCategory GetThreatCategory(const std::string& name); +const std::string& GetThreatCategoryName(ThreatCategory threatCategory); + +} // namespace awips +} // namespace scwx diff --git a/wxdata/include/scwx/awips/text_product_message.hpp b/wxdata/include/scwx/awips/text_product_message.hpp index 80ba4a16..a83f7b82 100644 --- a/wxdata/include/scwx/awips/text_product_message.hpp +++ b/wxdata/include/scwx/awips/text_product_message.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -56,15 +57,16 @@ struct SegmentHeader struct Segment { - std::optional header_; - std::vector productContent_; - std::optional codedLocation_; - std::optional codedMotion_; + std::optional header_ {}; + std::vector productContent_ {}; + std::optional codedLocation_ {}; + std::optional codedMotion_ {}; - Segment() : - header_ {}, productContent_ {}, codedLocation_ {}, codedMotion_ {} - { - } + bool observed_ {false}; + ThreatCategory threatCategory_ {ThreatCategory::Base}; + bool tornadoPossible_ {false}; + + Segment() = default; Segment(const Segment&) = delete; Segment& operator=(const Segment&) = delete; diff --git a/wxdata/include/scwx/common/vcp.hpp b/wxdata/include/scwx/common/vcp.hpp index 9d95c7aa..d93a056a 100644 --- a/wxdata/include/scwx/common/vcp.hpp +++ b/wxdata/include/scwx/common/vcp.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include namespace scwx @@ -7,7 +8,7 @@ namespace scwx namespace common { -std::string GetVcpDescription(uint16_t vcp); +std::string GetVcpDescription(std::uint16_t vcp); } // namespace common } // namespace scwx diff --git a/wxdata/include/scwx/util/digest.hpp b/wxdata/include/scwx/util/digest.hpp index e0bbc3c9..e00727ca 100644 --- a/wxdata/include/scwx/util/digest.hpp +++ b/wxdata/include/scwx/util/digest.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/wxdata/include/scwx/util/strings.hpp b/wxdata/include/scwx/util/strings.hpp index e2821331..ea22d117 100644 --- a/wxdata/include/scwx/util/strings.hpp +++ b/wxdata/include/scwx/util/strings.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include diff --git a/wxdata/source/scwx/awips/impact_based_warnings.cpp b/wxdata/source/scwx/awips/impact_based_warnings.cpp new file mode 100644 index 00000000..75f04d1e --- /dev/null +++ b/wxdata/source/scwx/awips/impact_based_warnings.cpp @@ -0,0 +1,31 @@ +#include +#include + +#include + +#include + +namespace scwx +{ +namespace awips +{ + +static const std::string logPrefix_ = "scwx::awips::impact_based_warnings"; + +static const std::unordered_map + threatCategoryName_ {{ThreatCategory::Base, "Base"}, + {ThreatCategory::Significant, "Significant"}, + {ThreatCategory::Considerable, "Considerable"}, + {ThreatCategory::Destructive, "Destructive"}, + {ThreatCategory::Catastrophic, "Catastrophic"}, + {ThreatCategory::Unknown, "?"}}; + +SCWX_GET_ENUM(ThreatCategory, GetThreatCategory, threatCategoryName_) + +const std::string& GetThreatCategoryName(ThreatCategory threatCategory) +{ + return threatCategoryName_.at(threatCategory); +} + +} // namespace awips +} // namespace scwx diff --git a/wxdata/source/scwx/awips/text_product_message.cpp b/wxdata/source/scwx/awips/text_product_message.cpp index ffbd41dc..cccd9e07 100644 --- a/wxdata/source/scwx/awips/text_product_message.cpp +++ b/wxdata/source/scwx/awips/text_product_message.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -304,6 +305,14 @@ void ParseCodedInformation(std::shared_ptr segment, { typedef std::vector::const_iterator StringIterator; + static constexpr std::size_t kThreatCategoryTagCount = 4; + static const std::array + kThreatCategoryTags {"FLASH FLOOD DAMAGE THREAT...", + "SNOW SQUALL IMPACT...", + "THUNDERSTORM DAMAGE THREAT...", + "TORNADO DAMAGE THREAT..."}; + std::array::const_iterator threatTagIt; + std::vector& productContent = segment->productContent_; StringIterator codedLocationBegin = productContent.cend(); @@ -325,8 +334,8 @@ void ParseCodedInformation(std::shared_ptr segment, codedLocationEnd = it; } - if (codedMotionBegin == productContent.cend() && - it->starts_with("TIME...MOT...LOC")) + else if (codedMotionBegin == productContent.cend() && + it->starts_with("TIME...MOT...LOC")) { codedMotionBegin = it; } @@ -338,6 +347,37 @@ void ParseCodedInformation(std::shared_ptr segment, { codedMotionEnd = it; } + + else if (!segment->observed_ && + it->find("...OBSERVED") != std::string::npos) + { + segment->observed_ = true; + } + + else if (!segment->tornadoPossible_ && *it == "TORNADO...POSSIBLE") + { + segment->tornadoPossible_ = true; + } + + else if (segment->threatCategory_ == ThreatCategory::Base && + (threatTagIt = std::find_if(kThreatCategoryTags.cbegin(), + kThreatCategoryTags.cend(), + [&it](const std::string& tag) { + return it->starts_with(tag); + })) != kThreatCategoryTags.cend() && + it->length() > threatTagIt->length()) + { + const std::string threatCategoryName = + it->substr(threatTagIt->length()); + + ThreatCategory threatCategory = GetThreatCategory(threatCategoryName); + if (threatCategory == ThreatCategory::Unknown) + { + threatCategory = ThreatCategory::Base; + } + + segment->threatCategory_ = threatCategory; + } } if (codedLocationBegin != productContent.cend()) diff --git a/wxdata/source/scwx/util/time.cpp b/wxdata/source/scwx/util/time.cpp index 5f04c8f5..5cf091fe 100644 --- a/wxdata/source/scwx/util/time.cpp +++ b/wxdata/source/scwx/util/time.cpp @@ -62,10 +62,13 @@ std::string TimeString(std::chrono::system_clock::time_point time, #if defined(_MSC_VER) # define FORMAT_STRING_24_HOUR "{:%Y-%m-%d %H:%M:%S %Z}" # define FORMAT_STRING_12_HOUR "{:%Y-%m-%d %I:%M:%S %p %Z}" + namespace date = std::chrono; + namespace df = std; #else # define FORMAT_STRING_24_HOUR "%Y-%m-%d %H:%M:%S %Z" # define FORMAT_STRING_12_HOUR "%Y-%m-%d %I:%M:%S %p %Z" using namespace date; + namespace df = date; #endif auto timeInSeconds = time_point_cast(time); @@ -77,15 +80,15 @@ std::string TimeString(std::chrono::system_clock::time_point time, { try { - zoned_time zt = {timeZone, timeInSeconds}; + date::zoned_time zt = {timeZone, timeInSeconds}; if (clockFormat == ClockFormat::_24Hour) { - os << format(FORMAT_STRING_24_HOUR, zt); + os << df::format(FORMAT_STRING_24_HOUR, zt); } else { - os << format(FORMAT_STRING_12_HOUR, zt); + os << df::format(FORMAT_STRING_12_HOUR, zt); } } catch (const std::exception& ex) @@ -107,11 +110,11 @@ std::string TimeString(std::chrono::system_clock::time_point time, { if (clockFormat == ClockFormat::_24Hour) { - os << format(FORMAT_STRING_24_HOUR, timeInSeconds); + os << df::format(FORMAT_STRING_24_HOUR, timeInSeconds); } else { - os << format(FORMAT_STRING_12_HOUR, timeInSeconds); + os << df::format(FORMAT_STRING_12_HOUR, timeInSeconds); } } } diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index 0ce08cbb..47ada181 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -14,6 +14,7 @@ endif() set(HDR_AWIPS include/scwx/awips/coded_location.hpp include/scwx/awips/coded_time_motion_location.hpp + include/scwx/awips/impact_based_warnings.hpp include/scwx/awips/message.hpp include/scwx/awips/phenomenon.hpp include/scwx/awips/pvtec.hpp @@ -24,6 +25,7 @@ set(HDR_AWIPS include/scwx/awips/coded_location.hpp include/scwx/awips/wmo_header.hpp) set(SRC_AWIPS source/scwx/awips/coded_location.cpp source/scwx/awips/coded_time_motion_location.cpp + source/scwx/awips/impact_based_warnings.cpp source/scwx/awips/message.cpp source/scwx/awips/phenomenon.cpp source/scwx/awips/pvtec.cpp