mirror of
				https://github.com/ciphervance/supercell-wx.git
				synced 2025-10-31 13:00:05 +00:00 
			
		
		
		
	Merge pull request #425 from dpaulat/feature/archive-warnings
Archive Warnings
This commit is contained in:
		
						commit
						b084ccb1f4
					
				
					 53 changed files with 2462 additions and 726 deletions
				
			
		|  | @ -6,10 +6,11 @@ Checks: | ||||||
|   - 'misc-*' |   - 'misc-*' | ||||||
|   - 'modernize-*' |   - 'modernize-*' | ||||||
|   - 'performance-*' |   - 'performance-*' | ||||||
|  |   - '-bugprone-easily-swappable-parameters' | ||||||
|   - '-cppcoreguidelines-pro-type-reinterpret-cast' |   - '-cppcoreguidelines-pro-type-reinterpret-cast' | ||||||
|   - '-misc-include-cleaner' |   - '-misc-include-cleaner' | ||||||
|   - '-misc-non-private-member-variables-in-classes' |   - '-misc-non-private-member-variables-in-classes' | ||||||
|   - '-modernize-use-trailing-return-type' |   - '-misc-use-anonymous-namespace' | ||||||
|   - '-bugprone-easily-swappable-parameters' |  | ||||||
|   - '-modernize-return-braced-init-list' |   - '-modernize-return-braced-init-list' | ||||||
|  |   - '-modernize-use-trailing-return-type' | ||||||
| FormatStyle: 'file' | FormatStyle: 'file' | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								.github/workflows/clang-tidy-review.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/clang-tidy-review.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -126,7 +126,7 @@ jobs: | ||||||
|                --build_dir='../build' \ |                --build_dir='../build' \ | ||||||
|                --base_dir='${{ github.workspace }}/source' \ |                --base_dir='${{ github.workspace }}/source' \ | ||||||
|                --clang_tidy_checks='' \ |                --clang_tidy_checks='' \ | ||||||
|                --config_file='.clang-tidy' \ |                --config_file='' \ | ||||||
|                --include='*.[ch],*.[ch]xx,*.[chi]pp,*.[ch]++,*.cc,*.hh' \ |                --include='*.[ch],*.[ch]xx,*.[chi]pp,*.[ch]++,*.cc,*.hh' \ | ||||||
|                --exclude='' \ |                --exclude='' \ | ||||||
|                --apt-packages='' \ |                --apt-packages='' \ | ||||||
|  |  | ||||||
|  | @ -36,6 +36,7 @@ Supercell Wx uses code from the following dependencies: | ||||||
| | [OpenSSL](https://www.openssl.org/) | [OpenSSL License](https://spdx.org/licenses/OpenSSL.html) | | | [OpenSSL](https://www.openssl.org/) | [OpenSSL License](https://spdx.org/licenses/OpenSSL.html) | | ||||||
| | [Qt](https://www.qt.io/) | [GNU Lesser General Public License v3.0 only](https://spdx.org/licenses/LGPL-3.0-only.html) | Qt Core, Qt GUI, Qt Multimedia, Qt Network, Qt OpenGL, Qt Positioning, Qt Serial Port, Qt SQL, Qt SVG, Qt Widgets<br/>Additional Licenses: https://doc.qt.io/qt-6/licenses-used-in-qt.html | | | [Qt](https://www.qt.io/) | [GNU Lesser General Public License v3.0 only](https://spdx.org/licenses/LGPL-3.0-only.html) | Qt Core, Qt GUI, Qt Multimedia, Qt Network, Qt OpenGL, Qt Positioning, Qt Serial Port, Qt SQL, Qt SVG, Qt Widgets<br/>Additional Licenses: https://doc.qt.io/qt-6/licenses-used-in-qt.html | | ||||||
| | [qt6ct](https://github.com/trialuser02/qt6ct) | [BSD 2-Clause "Simplified" License](https://spdx.org/licenses/BSD-2-Clause.html) | | | [qt6ct](https://github.com/trialuser02/qt6ct) | [BSD 2-Clause "Simplified" License](https://spdx.org/licenses/BSD-2-Clause.html) | | ||||||
|  | | [range-v3](https://github.com/ericniebler/range-v3) | [Boost Software License 1.0](https://spdx.org/licenses/BSL-1.0.html)<br/>[MIT License](https://spdx.org/licenses/MIT.html)<br/>[Stepanov and McJones, "Elements of Programming" license](https://github.com/ericniebler/range-v3/tree/0.12.0?tab=License-1-ov-file)<br/>[SGI C++ Standard Template Library license](https://github.com/ericniebler/range-v3/tree/0.12.0?tab=License-1-ov-file) | | ||||||
| | [re2](https://github.com/google/re2) | [BSD 3-Clause "New" or "Revised" License](https://spdx.org/licenses/BSD-3-Clause.html) | | | [re2](https://github.com/google/re2) | [BSD 3-Clause "New" or "Revised" License](https://spdx.org/licenses/BSD-3-Clause.html) | | ||||||
| | [spdlog](https://github.com/gabime/spdlog) | [MIT License](https://spdx.org/licenses/MIT.html) | | | [spdlog](https://github.com/gabime/spdlog) | [MIT License](https://spdx.org/licenses/MIT.html) | | ||||||
| | [SQLite](https://www.sqlite.org/) | Public Domain | | | [SQLite](https://www.sqlite.org/) | Public Domain | | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ class SupercellWxConan(ConanFile): | ||||||
|                   "libpng/1.6.47", |                   "libpng/1.6.47", | ||||||
|                   "libxml2/2.13.6", |                   "libxml2/2.13.6", | ||||||
|                   "openssl/3.4.1", |                   "openssl/3.4.1", | ||||||
|  |                   "range-v3/0.12.0", | ||||||
|                   "re2/20240702", |                   "re2/20240702", | ||||||
|                   "spdlog/1.15.1", |                   "spdlog/1.15.1", | ||||||
|                   "sqlite3/3.49.1", |                   "sqlite3/3.49.1", | ||||||
|  |  | ||||||
|  | @ -11,6 +11,8 @@ set(CMAKE_AUTORCC ON) | ||||||
| set(CMAKE_CXX_STANDARD 20) | set(CMAKE_CXX_STANDARD 20) | ||||||
| set(CMAKE_CXX_STANDARD_REQUIRED ON) | set(CMAKE_CXX_STANDARD_REQUIRED ON) | ||||||
| 
 | 
 | ||||||
|  | OPTION(SCWX_DISABLE_CONSOLE "Disables the Windows console in release mode" ON) | ||||||
|  | 
 | ||||||
| find_package(Boost) | find_package(Boost) | ||||||
| find_package(Fontconfig) | find_package(Fontconfig) | ||||||
| find_package(geographiclib) | find_package(geographiclib) | ||||||
|  | @ -615,7 +617,9 @@ set_target_properties(scwx-qt_update_radar_sites   PROPERTIES FOLDER generate) | ||||||
| if (WIN32) | if (WIN32) | ||||||
|     set(APP_ICON_RESOURCE_WINDOWS ${RESOURCE_OUTPUT}) |     set(APP_ICON_RESOURCE_WINDOWS ${RESOURCE_OUTPUT}) | ||||||
|     qt_add_executable(supercell-wx ${EXECUTABLE_SOURCES} ${APP_ICON_RESOURCE_WINDOWS}) |     qt_add_executable(supercell-wx ${EXECUTABLE_SOURCES} ${APP_ICON_RESOURCE_WINDOWS}) | ||||||
|  |     if (SCWX_DISABLE_CONSOLE) | ||||||
|         set_target_properties(supercell-wx PROPERTIES WIN32_EXECUTABLE $<IF:$<CONFIG:Release>,TRUE,FALSE>) |         set_target_properties(supercell-wx PROPERTIES WIN32_EXECUTABLE $<IF:$<CONFIG:Release>,TRUE,FALSE>) | ||||||
|  |     endif() | ||||||
| else() | else() | ||||||
|     qt_add_executable(supercell-wx ${EXECUTABLE_SOURCES}) |     qt_add_executable(supercell-wx ${EXECUTABLE_SOURCES}) | ||||||
| endif() | endif() | ||||||
|  |  | ||||||
|  | @ -245,7 +245,7 @@ size_t RadarSite::ReadConfig(const std::string& path) | ||||||
|    bool   dataValid  = true; |    bool   dataValid  = true; | ||||||
|    size_t sitesAdded = 0; |    size_t sitesAdded = 0; | ||||||
| 
 | 
 | ||||||
|    boost::json::value j = util::json::ReadJsonFile(path); |    boost::json::value j = util::json::ReadJsonQFile(path); | ||||||
| 
 | 
 | ||||||
|    dataValid = j.is_array(); |    dataValid = j.is_array(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1018,6 +1018,7 @@ void MainWindowImpl::ConnectAnimationSignals() | ||||||
|               for (auto map : maps_) |               for (auto map : maps_) | ||||||
|               { |               { | ||||||
|                  map->SelectTime(dateTime); |                  map->SelectTime(dateTime); | ||||||
|  |                  textEventManager_->SelectTime(dateTime); | ||||||
|                  QMetaObject::invokeMethod( |                  QMetaObject::invokeMethod( | ||||||
|                     map, static_cast<void (QWidget::*)()>(&QWidget::update)); |                     map, static_cast<void (QWidget::*)()>(&QWidget::update)); | ||||||
|               } |               } | ||||||
|  |  | ||||||
|  | @ -138,8 +138,10 @@ common::Coordinate AlertManager::Impl::CurrentCoordinate( | ||||||
| void AlertManager::Impl::HandleAlert(const types::TextEventKey& key, | void AlertManager::Impl::HandleAlert(const types::TextEventKey& key, | ||||||
|                                      size_t messageIndex) const |                                      size_t messageIndex) const | ||||||
| { | { | ||||||
|  |    auto messages = textEventManager_->message_list(key); | ||||||
|  | 
 | ||||||
|    // Skip alert if there are more messages to be processed
 |    // Skip alert if there are more messages to be processed
 | ||||||
|    if (messageIndex + 1 < textEventManager_->message_count(key)) |    if (messages.empty() || messageIndex + 1 < messages.size()) | ||||||
|    { |    { | ||||||
|       return; |       return; | ||||||
|    } |    } | ||||||
|  | @ -153,7 +155,7 @@ void AlertManager::Impl::HandleAlert(const types::TextEventKey& key, | ||||||
|       audioSettings.alert_radius().GetValue()); |       audioSettings.alert_radius().GetValue()); | ||||||
|    std::string alertWFO = audioSettings.alert_wfo().GetValue(); |    std::string alertWFO = audioSettings.alert_wfo().GetValue(); | ||||||
| 
 | 
 | ||||||
|    auto message = textEventManager_->message_list(key).at(messageIndex); |    auto message = messages.at(messageIndex); | ||||||
| 
 | 
 | ||||||
|    for (auto& segment : message->segments()) |    for (auto& segment : message->segments()) | ||||||
|    { |    { | ||||||
|  |  | ||||||
|  | @ -1,14 +1,15 @@ | ||||||
| #include <scwx/qt/manager/marker_manager.hpp> | #include <scwx/qt/manager/marker_manager.hpp> | ||||||
| #include <scwx/qt/types/marker_types.hpp> | #include <scwx/qt/types/marker_types.hpp> | ||||||
| #include <scwx/qt/util/color.hpp> | #include <scwx/qt/util/color.hpp> | ||||||
| #include <scwx/qt/util/json.hpp> |  | ||||||
| #include <scwx/qt/util/texture_atlas.hpp> | #include <scwx/qt/util/texture_atlas.hpp> | ||||||
| #include <scwx/qt/main/application.hpp> | #include <scwx/qt/main/application.hpp> | ||||||
| #include <scwx/qt/manager/resource_manager.hpp> | #include <scwx/qt/manager/resource_manager.hpp> | ||||||
|  | #include <scwx/util/json.hpp> | ||||||
| #include <scwx/util/logger.hpp> | #include <scwx/util/logger.hpp> | ||||||
| 
 | 
 | ||||||
| #include <filesystem> | #include <filesystem> | ||||||
| #include <shared_mutex> | #include <shared_mutex> | ||||||
|  | #include <utility> | ||||||
| #include <vector> | #include <vector> | ||||||
| #include <string> | #include <string> | ||||||
| #include <unordered_map> | #include <unordered_map> | ||||||
|  | @ -70,15 +71,9 @@ public: | ||||||
| class MarkerManager::Impl::MarkerRecord | class MarkerManager::Impl::MarkerRecord | ||||||
| { | { | ||||||
| public: | public: | ||||||
|    MarkerRecord(const types::MarkerInfo& info) : |    MarkerRecord(types::MarkerInfo info) : markerInfo_ {std::move(info)} {} | ||||||
|       markerInfo_ {info} |  | ||||||
|    { |  | ||||||
|    } |  | ||||||
| 
 | 
 | ||||||
|    const types::MarkerInfo& toMarkerInfo() |    const types::MarkerInfo& toMarkerInfo() { return markerInfo_; } | ||||||
|    { |  | ||||||
|       return markerInfo_; |  | ||||||
|    } |  | ||||||
| 
 | 
 | ||||||
|    types::MarkerInfo markerInfo_; |    types::MarkerInfo markerInfo_; | ||||||
| 
 | 
 | ||||||
|  | @ -175,7 +170,7 @@ void MarkerManager::Impl::ReadMarkerSettings() | ||||||
|       // Determine if marker settings exists
 |       // Determine if marker settings exists
 | ||||||
|       if (std::filesystem::exists(markerSettingsPath_)) |       if (std::filesystem::exists(markerSettingsPath_)) | ||||||
|       { |       { | ||||||
|          markerJson = util::json::ReadJsonFile(markerSettingsPath_); |          markerJson = scwx::util::json::ReadJsonFile(markerSettingsPath_); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (markerJson != nullptr && markerJson.is_array()) |       if (markerJson != nullptr && markerJson.is_array()) | ||||||
|  | @ -225,7 +220,7 @@ void MarkerManager::Impl::WriteMarkerSettings() | ||||||
| 
 | 
 | ||||||
|    const std::shared_lock lock(markerRecordLock_); |    const std::shared_lock lock(markerRecordLock_); | ||||||
|    auto                   markerJson = boost::json::value_from(markerRecords_); |    auto                   markerJson = boost::json::value_from(markerRecords_); | ||||||
|    util::json::WriteJsonFile(markerSettingsPath_, markerJson); |    scwx::util::json::WriteJsonFile(markerSettingsPath_, markerJson); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| std::shared_ptr<MarkerManager::Impl::MarkerRecord> | std::shared_ptr<MarkerManager::Impl::MarkerRecord> | ||||||
|  | @ -360,7 +355,8 @@ types::MarkerId MarkerManager::add_marker(const types::MarkerInfo& marker) | ||||||
|       id           = p->NewId(); |       id           = p->NewId(); | ||||||
|       size_t index = p->markerRecords_.size(); |       size_t index = p->markerRecords_.size(); | ||||||
|       p->idToIndex_.emplace(id, index); |       p->idToIndex_.emplace(id, index); | ||||||
|       p->markerRecords_.emplace_back(std::make_shared<Impl::MarkerRecord>(marker)); |       p->markerRecords_.emplace_back( | ||||||
|  |          std::make_shared<Impl::MarkerRecord>(marker)); | ||||||
|       p->markerRecords_[index]->markerInfo_.id = id; |       p->markerRecords_[index]->markerInfo_.id = id; | ||||||
| 
 | 
 | ||||||
|       add_icon(marker.iconName); |       add_icon(marker.iconName); | ||||||
|  | @ -499,7 +495,6 @@ void MarkerManager::set_marker_settings_path(const std::string& path) | ||||||
|    p->markerSettingsPath_ = path; |    p->markerSettingsPath_ = path; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| std::shared_ptr<MarkerManager> MarkerManager::Instance() | std::shared_ptr<MarkerManager> MarkerManager::Instance() | ||||||
| { | { | ||||||
|    static std::weak_ptr<MarkerManager> markerManagerReference_ {}; |    static std::weak_ptr<MarkerManager> markerManagerReference_ {}; | ||||||
|  |  | ||||||
|  | @ -2,10 +2,10 @@ | ||||||
| #include <scwx/qt/manager/font_manager.hpp> | #include <scwx/qt/manager/font_manager.hpp> | ||||||
| #include <scwx/qt/manager/resource_manager.hpp> | #include <scwx/qt/manager/resource_manager.hpp> | ||||||
| #include <scwx/qt/main/application.hpp> | #include <scwx/qt/main/application.hpp> | ||||||
| #include <scwx/qt/util/json.hpp> |  | ||||||
| #include <scwx/qt/util/network.hpp> | #include <scwx/qt/util/network.hpp> | ||||||
| #include <scwx/gr/placefile.hpp> | #include <scwx/gr/placefile.hpp> | ||||||
| #include <scwx/network/cpr.hpp> | #include <scwx/network/cpr.hpp> | ||||||
|  | #include <scwx/util/json.hpp> | ||||||
| #include <scwx/util/logger.hpp> | #include <scwx/util/logger.hpp> | ||||||
| 
 | 
 | ||||||
| #include <shared_mutex> | #include <shared_mutex> | ||||||
|  | @ -385,7 +385,7 @@ void PlacefileManager::Impl::ReadPlacefileSettings() | ||||||
|    // Determine if placefile settings exists
 |    // Determine if placefile settings exists
 | ||||||
|    if (std::filesystem::exists(placefileSettingsPath_)) |    if (std::filesystem::exists(placefileSettingsPath_)) | ||||||
|    { |    { | ||||||
|       placefileJson = util::json::ReadJsonFile(placefileSettingsPath_); |       placefileJson = scwx::util::json::ReadJsonFile(placefileSettingsPath_); | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|    // If placefile settings was successfully read
 |    // If placefile settings was successfully read
 | ||||||
|  | @ -428,7 +428,7 @@ void PlacefileManager::Impl::WritePlacefileSettings() | ||||||
| 
 | 
 | ||||||
|    std::shared_lock lock {placefileRecordLock_}; |    std::shared_lock lock {placefileRecordLock_}; | ||||||
|    auto             placefileJson = boost::json::value_from(placefileRecords_); |    auto             placefileJson = boost::json::value_from(placefileRecords_); | ||||||
|    util::json::WriteJsonFile(placefileSettingsPath_, placefileJson); |    scwx::util::json::WriteJsonFile(placefileSettingsPath_, placefileJson); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void PlacefileManager::SetRadarSite( | void PlacefileManager::SetRadarSite( | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ | ||||||
| #include <scwx/qt/settings/text_settings.hpp> | #include <scwx/qt/settings/text_settings.hpp> | ||||||
| #include <scwx/qt/settings/ui_settings.hpp> | #include <scwx/qt/settings/ui_settings.hpp> | ||||||
| #include <scwx/qt/settings/unit_settings.hpp> | #include <scwx/qt/settings/unit_settings.hpp> | ||||||
| #include <scwx/qt/util/json.hpp> | #include <scwx/util/json.hpp> | ||||||
| #include <scwx/util/logger.hpp> | #include <scwx/util/logger.hpp> | ||||||
| 
 | 
 | ||||||
| #include <filesystem> | #include <filesystem> | ||||||
|  |  | ||||||
|  | @ -2,28 +2,61 @@ | ||||||
| #include <scwx/qt/main/application.hpp> | #include <scwx/qt/main/application.hpp> | ||||||
| #include <scwx/qt/settings/general_settings.hpp> | #include <scwx/qt/settings/general_settings.hpp> | ||||||
| #include <scwx/awips/text_product_file.hpp> | #include <scwx/awips/text_product_file.hpp> | ||||||
|  | #include <scwx/provider/iem_api_provider.ipp> | ||||||
| #include <scwx/provider/warnings_provider.hpp> | #include <scwx/provider/warnings_provider.hpp> | ||||||
| #include <scwx/util/logger.hpp> | #include <scwx/util/logger.hpp> | ||||||
|  | #include <scwx/util/time.hpp> | ||||||
| 
 | 
 | ||||||
|  | #include <algorithm> | ||||||
|  | #include <list> | ||||||
|  | #include <map> | ||||||
| #include <shared_mutex> | #include <shared_mutex> | ||||||
| #include <unordered_map> | #include <unordered_map> | ||||||
| 
 | 
 | ||||||
| #include <boost/asio/post.hpp> | #include <boost/asio/post.hpp> | ||||||
| #include <boost/asio/steady_timer.hpp> | #include <boost/asio/steady_timer.hpp> | ||||||
| #include <boost/asio/thread_pool.hpp> | #include <boost/asio/thread_pool.hpp> | ||||||
|  | #include <boost/container/stable_vector.hpp> | ||||||
|  | #include <boost/range/irange.hpp> | ||||||
|  | #include <range/v3/range/conversion.hpp> | ||||||
|  | #include <range/v3/view/filter.hpp> | ||||||
|  | #include <range/v3/view/single.hpp> | ||||||
|  | #include <range/v3/view/transform.hpp> | ||||||
| 
 | 
 | ||||||
| namespace scwx | #if (__cpp_lib_chrono < 201907L) | ||||||
| { | #   include <date/date.h> | ||||||
| namespace qt | #endif | ||||||
| { | 
 | ||||||
| namespace manager | namespace scwx::qt::manager | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
|  | using namespace std::chrono_literals; | ||||||
|  | 
 | ||||||
| static const std::string logPrefix_ = "scwx::qt::manager::text_event_manager"; | static const std::string logPrefix_ = "scwx::qt::manager::text_event_manager"; | ||||||
| static const auto        logger_    = scwx::util::Logger::Create(logPrefix_); | static const auto        logger_    = scwx::util::Logger::Create(logPrefix_); | ||||||
| 
 | 
 | ||||||
| static const std::string& kDefaultWarningsProviderUrl { | static constexpr std::chrono::hours kInitialLoadHistoryDuration_ = | ||||||
|    "https://warnings.allisonhouse.com"}; |    std::chrono::days {3}; | ||||||
|  | static constexpr std::chrono::hours kDefaultLoadHistoryDuration_ = | ||||||
|  |    std::chrono::hours {1}; | ||||||
|  | 
 | ||||||
|  | static const std::array<std::string, 8> kPils_ = { | ||||||
|  |    "FFS", "FFW", "MWS", "SMW", "SQW", "SVR", "SVS", "TOR"}; | ||||||
|  | 
 | ||||||
|  | static const std:: | ||||||
|  |    unordered_map<std::string, std::pair<std::chrono::hours, std::chrono::hours>> | ||||||
|  |       kPilLoadWindows_ {{"FFS", {-24h, 1h}}, | ||||||
|  |                         {"FFW", {-24h, 1h}}, | ||||||
|  |                         {"MWS", {-4h, 1h}}, | ||||||
|  |                         {"SMW", {-4h, 1h}}, | ||||||
|  |                         {"SQW", {-4h, 1h}}, | ||||||
|  |                         {"SVR", {-4h, 1h}}, | ||||||
|  |                         {"SVS", {-4h, 1h}}, | ||||||
|  |                         {"TOR", {-4h, 1h}}}; | ||||||
|  | 
 | ||||||
|  | // Widest load window provided by kPilLoadWindows_
 | ||||||
|  | static const std::pair<std::chrono::hours, std::chrono::hours> | ||||||
|  |    kArchiveLoadWindow_ {-24h, 1h}; | ||||||
| 
 | 
 | ||||||
| class TextEventManager::Impl | class TextEventManager::Impl | ||||||
| { | { | ||||||
|  | @ -42,7 +75,9 @@ public: | ||||||
| 
 | 
 | ||||||
|       warningsProviderChangedCallbackUuid_ = |       warningsProviderChangedCallbackUuid_ = | ||||||
|          generalSettings.warnings_provider().RegisterValueChangedCallback( |          generalSettings.warnings_provider().RegisterValueChangedCallback( | ||||||
|             [this](const std::string& value) { |             [this](const std::string& value) | ||||||
|  |             { | ||||||
|  |                loadHistoryDuration_ = kInitialLoadHistoryDuration_; | ||||||
|                warningsProvider_ = |                warningsProvider_ = | ||||||
|                   std::make_shared<provider::WarningsProvider>(value); |                   std::make_shared<provider::WarningsProvider>(value); | ||||||
|             }); |             }); | ||||||
|  | @ -76,11 +111,30 @@ public: | ||||||
|       threadPool_.join(); |       threadPool_.join(); | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|    void HandleMessage(std::shared_ptr<awips::TextProductMessage> message); |    Impl(const Impl&)             = delete; | ||||||
|  |    Impl& operator=(const Impl&)  = delete; | ||||||
|  |    Impl(const Impl&&)            = delete; | ||||||
|  |    Impl& operator=(const Impl&&) = delete; | ||||||
|  | 
 | ||||||
|  |    void HandleMessage(const std::shared_ptr<awips::TextProductMessage>& message, | ||||||
|  |                       bool archiveEvent = false); | ||||||
|  |    template<ranges::forward_range DateRange> | ||||||
|  |       requires std::same_as<ranges::range_value_t<DateRange>, | ||||||
|  |                             std::chrono::sys_days> | ||||||
|  |    void ListArchives(DateRange dates); | ||||||
|  |    void LoadArchives(std::chrono::system_clock::time_point dateTime); | ||||||
|  |    void PruneArchives(); | ||||||
|    void RefreshAsync(); |    void RefreshAsync(); | ||||||
|    void Refresh(); |    void Refresh(); | ||||||
|  |    template<ranges::forward_range DateRange> | ||||||
|  |       requires std::same_as<ranges::range_value_t<DateRange>, | ||||||
|  |                             std::chrono::sys_days> | ||||||
|  |    void UpdateArchiveDates(DateRange dates); | ||||||
| 
 | 
 | ||||||
|    boost::asio::thread_pool threadPool_ {1u}; |    // Thread pool sized for:
 | ||||||
|  |    // - Live Refresh (1x)
 | ||||||
|  |    // - Archive Loading (1x)
 | ||||||
|  |    boost::asio::thread_pool threadPool_ {2u}; | ||||||
| 
 | 
 | ||||||
|    TextEventManager* self_; |    TextEventManager* self_; | ||||||
| 
 | 
 | ||||||
|  | @ -95,6 +149,27 @@ public: | ||||||
| 
 | 
 | ||||||
|    std::shared_ptr<provider::WarningsProvider> warningsProvider_ {nullptr}; |    std::shared_ptr<provider::WarningsProvider> warningsProvider_ {nullptr}; | ||||||
| 
 | 
 | ||||||
|  |    std::chrono::hours loadHistoryDuration_ {kInitialLoadHistoryDuration_}; | ||||||
|  |    std::chrono::sys_time<std::chrono::hours> prevLoadTime_ {}; | ||||||
|  |    std::chrono::sys_days                     archiveLimit_ {}; | ||||||
|  | 
 | ||||||
|  |    std::mutex                       archiveMutex_ {}; | ||||||
|  |    std::list<std::chrono::sys_days> archiveDates_ {}; | ||||||
|  | 
 | ||||||
|  |    std::mutex archiveEventKeyMutex_ {}; | ||||||
|  |    std::map<std::chrono::sys_days, | ||||||
|  |             std::unordered_set<types::TextEventKey, | ||||||
|  |                                types::TextEventHash<types::TextEventKey>>> | ||||||
|  |       archiveEventKeys_ {}; | ||||||
|  |    std::unordered_set<types::TextEventKey, | ||||||
|  |                       types::TextEventHash<types::TextEventKey>> | ||||||
|  |       liveEventKeys_ {}; | ||||||
|  | 
 | ||||||
|  |    std::mutex unloadedProductMapMutex_ {}; | ||||||
|  |    std::map<std::chrono::sys_days, | ||||||
|  |             boost::container::stable_vector<scwx::types::iem::AfosEntry>> | ||||||
|  |       unloadedProductMap_; | ||||||
|  | 
 | ||||||
|    boost::uuids::uuid warningsProviderChangedCallbackUuid_ {}; |    boost::uuids::uuid warningsProviderChangedCallbackUuid_ {}; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -164,9 +239,56 @@ void TextEventManager::LoadFile(const std::string& filename) | ||||||
|                      }); |                      }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void TextEventManager::Impl::HandleMessage( | void TextEventManager::SelectTime( | ||||||
|    std::shared_ptr<awips::TextProductMessage> message) |    std::chrono::system_clock::time_point dateTime) | ||||||
| { | { | ||||||
|  |    if (dateTime == std::chrono::system_clock::time_point {}) | ||||||
|  |    { | ||||||
|  |       // Ignore a default date/time selection
 | ||||||
|  |       return; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    logger_->trace("Select Time: {}", util::TimeString(dateTime)); | ||||||
|  | 
 | ||||||
|  |    boost::asio::post( | ||||||
|  |       p->threadPool_, | ||||||
|  |       [dateTime, this]() | ||||||
|  |       { | ||||||
|  |          try | ||||||
|  |          { | ||||||
|  |             const auto today = std::chrono::floor<std::chrono::days>(dateTime); | ||||||
|  |             const auto yesterday = today - std::chrono::days {1}; | ||||||
|  |             const auto tomorrow  = today + std::chrono::days {1}; | ||||||
|  |             const auto dateArray = std::array {yesterday, today, tomorrow}; | ||||||
|  | 
 | ||||||
|  |             const auto dates = | ||||||
|  |                dateArray | | ||||||
|  |                ranges::views::filter( | ||||||
|  |                   [this](const auto& date) | ||||||
|  |                   { | ||||||
|  |                      return p->archiveLimit_ == std::chrono::sys_days {} || | ||||||
|  |                             date < p->archiveLimit_; | ||||||
|  |                   }); | ||||||
|  | 
 | ||||||
|  |             const std::unique_lock lock {p->archiveMutex_}; | ||||||
|  | 
 | ||||||
|  |             p->UpdateArchiveDates(dates); | ||||||
|  |             p->ListArchives(dates); | ||||||
|  |             p->LoadArchives(dateTime); | ||||||
|  |             p->PruneArchives(); | ||||||
|  |          } | ||||||
|  |          catch (const std::exception& ex) | ||||||
|  |          { | ||||||
|  |             logger_->error(ex.what()); | ||||||
|  |          } | ||||||
|  |       }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void TextEventManager::Impl::HandleMessage( | ||||||
|  |    const std::shared_ptr<awips::TextProductMessage>& message, bool archiveEvent) | ||||||
|  | { | ||||||
|  |    using namespace std::chrono_literals; | ||||||
|  | 
 | ||||||
|    auto segments = message->segments(); |    auto segments = message->segments(); | ||||||
| 
 | 
 | ||||||
|    // If there are no segments, skip this message
 |    // If there are no segments, skip this message
 | ||||||
|  | @ -187,21 +309,49 @@ void TextEventManager::Impl::HandleMessage( | ||||||
|       } |       } | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|  |    // Determine year
 | ||||||
|  |    const std::chrono::year_month_day wmoDate = | ||||||
|  |       std::chrono::floor<std::chrono::days>( | ||||||
|  |          message->wmo_header()->GetDateTime()); | ||||||
|  |    const std::chrono::year wmoYear = wmoDate.year(); | ||||||
|  | 
 | ||||||
|    std::unique_lock lock(textEventMutex_); |    std::unique_lock lock(textEventMutex_); | ||||||
| 
 | 
 | ||||||
|    // Find a matching event in the event map
 |    // Find a matching event in the event map
 | ||||||
|    auto&               vtecString = segments[0]->header_->vtecString_; |    auto&               vtecString = segments[0]->header_->vtecString_; | ||||||
|    types::TextEventKey key {vtecString[0].pVtec_}; |    types::TextEventKey key {vtecString[0].pVtec_, wmoYear}; | ||||||
|    size_t              messageIndex = 0; |    size_t              messageIndex = 0; | ||||||
|    auto                it           = textEventMap_.find(key); |    auto                it           = textEventMap_.find(key); | ||||||
|    bool                updated      = false; |    bool                updated      = false; | ||||||
| 
 | 
 | ||||||
|  |    if ( | ||||||
|  |       // If there was no matching event
 | ||||||
|  |       it == textEventMap_.cend() && | ||||||
|  |       // The event is not new
 | ||||||
|  |       vtecString[0].pVtec_.action() != awips::PVtec::Action::New && | ||||||
|  |       // The message was on January 1
 | ||||||
|  |       wmoDate.month() == std::chrono::January && wmoDate.day() == 1d && | ||||||
|  |       // This is at least the 10th ETN of the year
 | ||||||
|  |       // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers): Readability
 | ||||||
|  |       vtecString[0].pVtec_.event_tracking_number() > 10) | ||||||
|  |    { | ||||||
|  |       // Attempt to find a matching event from last year
 | ||||||
|  |       key = {vtecString[0].pVtec_, wmoYear - std::chrono::years {1}}; | ||||||
|  |       it  = textEventMap_.find(key); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|    if (it == textEventMap_.cend()) |    if (it == textEventMap_.cend()) | ||||||
|    { |    { | ||||||
|       // If there was no matching event, add the message to a new event
 |       // If there was no matching event, add the message to a new event
 | ||||||
|       textEventMap_.emplace(key, std::vector {message}); |       textEventMap_.emplace(key, std::vector {message}); | ||||||
|       messageIndex = 0; |       messageIndex = 0; | ||||||
|       updated      = true; |       updated      = true; | ||||||
|  | 
 | ||||||
|  |       if (!archiveEvent) | ||||||
|  |       { | ||||||
|  |          // Add the Text Event Key to the list of live events to prevent pruning
 | ||||||
|  |          liveEventKeys_.insert(key); | ||||||
|  |       } | ||||||
|    } |    } | ||||||
|    else if (std::find_if(it->second.cbegin(), |    else if (std::find_if(it->second.cbegin(), | ||||||
|                          it->second.cend(), |                          it->second.cend(), | ||||||
|  | @ -214,16 +364,284 @@ void TextEventManager::Impl::HandleMessage( | ||||||
|       // If there was a matching event, and this message has not been stored
 |       // If there was a matching event, and this message has not been stored
 | ||||||
|       // (WMO header equivalence check), add the updated message to the existing
 |       // (WMO header equivalence check), add the updated message to the existing
 | ||||||
|       // event
 |       // event
 | ||||||
|       messageIndex = it->second.size(); | 
 | ||||||
|       it->second.push_back(message); |       // Determine the chronological sequence of the message. Note, if there
 | ||||||
|  |       // were no time hints given to the WMO header, this will place the message
 | ||||||
|  |       // at the end of the vector.
 | ||||||
|  |       auto insertionPoint = std::upper_bound( | ||||||
|  |          it->second.begin(), | ||||||
|  |          it->second.end(), | ||||||
|  |          message, | ||||||
|  |          [](const std::shared_ptr<awips::TextProductMessage>& a, | ||||||
|  |             const std::shared_ptr<awips::TextProductMessage>& b) | ||||||
|  |          { | ||||||
|  |             return a->wmo_header()->GetDateTime() < | ||||||
|  |                    b->wmo_header()->GetDateTime(); | ||||||
|  |          }); | ||||||
|  | 
 | ||||||
|  |       // Insert the message in chronological order
 | ||||||
|  |       messageIndex = std::distance(it->second.begin(), insertionPoint); | ||||||
|  |       it->second.insert(insertionPoint, message); | ||||||
|       updated = true; |       updated = true; | ||||||
|    }; |    }; | ||||||
| 
 | 
 | ||||||
|  |    // If this is an archive event, and the key does not exist in the live events
 | ||||||
|  |    // Assumption: A live event will always be loaded before a duplicate archive
 | ||||||
|  |    // event
 | ||||||
|  |    if (archiveEvent && !liveEventKeys_.contains(key)) | ||||||
|  |    { | ||||||
|  |       // Add the Text Event Key to the current date's archive
 | ||||||
|  |       const std::unique_lock archiveEventKeyLock {archiveEventKeyMutex_}; | ||||||
|  |       auto&                  archiveKeys = archiveEventKeys_[wmoDate]; | ||||||
|  |       archiveKeys.insert(key); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|    lock.unlock(); |    lock.unlock(); | ||||||
| 
 | 
 | ||||||
|    if (updated) |    if (updated) | ||||||
|    { |    { | ||||||
|       Q_EMIT self_->AlertUpdated(key, messageIndex); |       Q_EMIT self_->AlertUpdated(key, messageIndex, message->uuid()); | ||||||
|  |    } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | template<ranges::forward_range DateRange> | ||||||
|  |    requires std::same_as<ranges::range_value_t<DateRange>, | ||||||
|  |                          std::chrono::sys_days> | ||||||
|  | void TextEventManager::Impl::ListArchives(DateRange dates) | ||||||
|  | { | ||||||
|  |    // Don't reload data that has already been loaded
 | ||||||
|  |    auto filteredDates = | ||||||
|  |       dates | | ||||||
|  |       ranges::views::filter([this](const auto& date) | ||||||
|  |                             { return !unloadedProductMap_.contains(date); }); | ||||||
|  | 
 | ||||||
|  |    const auto dv = ranges::to<std::vector>(filteredDates); | ||||||
|  | 
 | ||||||
|  |    std::for_each( | ||||||
|  |       std::execution::par, | ||||||
|  |       dv.begin(), | ||||||
|  |       dv.end(), | ||||||
|  |       [this](const auto& date) | ||||||
|  |       { | ||||||
|  |          static const auto kEmptyRange_ = | ||||||
|  |             ranges::views::single(std::string_view {}); | ||||||
|  |          static const auto kPilsView_ = | ||||||
|  |             kPils_ | | ||||||
|  |             ranges::views::transform([](const std::string& pil) | ||||||
|  |                                      { return std::string_view {pil}; }); | ||||||
|  | 
 | ||||||
|  |          const auto dateArray = std::array {date}; | ||||||
|  | 
 | ||||||
|  |          auto productEntries = provider::IemApiProvider::ListTextProducts( | ||||||
|  |             dateArray | ranges::views::all, kEmptyRange_, kPilsView_); | ||||||
|  | 
 | ||||||
|  |          const std::unique_lock lock {unloadedProductMapMutex_}; | ||||||
|  | 
 | ||||||
|  |          if (productEntries.has_value()) | ||||||
|  |          { | ||||||
|  |             unloadedProductMap_.try_emplace( | ||||||
|  |                date, | ||||||
|  |                boost::container::stable_vector<scwx::types::iem::AfosEntry> { | ||||||
|  |                   std::make_move_iterator(productEntries.value().begin()), | ||||||
|  |                   std::make_move_iterator(productEntries.value().end())}); | ||||||
|  |          } | ||||||
|  |       }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void TextEventManager::Impl::LoadArchives( | ||||||
|  |    std::chrono::system_clock::time_point dateTime) | ||||||
|  | { | ||||||
|  |    using namespace std::chrono; | ||||||
|  | 
 | ||||||
|  | #if (__cpp_lib_chrono >= 201907L) | ||||||
|  |    namespace df = std; | ||||||
|  | 
 | ||||||
|  |    static constexpr std::string_view kDateFormat {"{:%Y%m%d%H%M}"}; | ||||||
|  | #else | ||||||
|  |    using namespace date; | ||||||
|  |    namespace df = date; | ||||||
|  | 
 | ||||||
|  | #   define kDateFormat "%Y%m%d%H%M" | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  |    // Search unloaded products in the widest archive load window
 | ||||||
|  |    const std::chrono::sys_days startDate = | ||||||
|  |       std::chrono::floor<std::chrono::days>(dateTime + | ||||||
|  |                                             kArchiveLoadWindow_.first); | ||||||
|  |    const std::chrono::sys_days endDate = std::chrono::floor<std::chrono::days>( | ||||||
|  |       dateTime + kArchiveLoadWindow_.second + std::chrono::days {1}); | ||||||
|  | 
 | ||||||
|  |    // Determine load windows for each PIL
 | ||||||
|  |    std::unordered_map<std::string, std::pair<std::string, std::string>> | ||||||
|  |       pilLoadWindowStrings; | ||||||
|  | 
 | ||||||
|  |    for (auto& loadWindow : kPilLoadWindows_) | ||||||
|  |    { | ||||||
|  |       const std::string& pil = loadWindow.first; | ||||||
|  | 
 | ||||||
|  |       pilLoadWindowStrings.insert_or_assign( | ||||||
|  |          pil, | ||||||
|  |          std::pair<std::string, std::string> { | ||||||
|  |             df::format(kDateFormat, (dateTime + loadWindow.second.first)), | ||||||
|  |             df::format(kDateFormat, (dateTime + loadWindow.second.second))}); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    std::vector<scwx::types::iem::AfosEntry> loadListEntries {}; | ||||||
|  | 
 | ||||||
|  |    for (auto date : boost::irange(startDate, endDate)) | ||||||
|  |    { | ||||||
|  |       auto mapIt = unloadedProductMap_.find(date); | ||||||
|  |       if (mapIt == unloadedProductMap_.cend()) | ||||||
|  |       { | ||||||
|  |          continue; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       for (auto it = mapIt->second.begin(); it != mapIt->second.end();) | ||||||
|  |       { | ||||||
|  |          const auto& pil = it->pil_; | ||||||
|  | 
 | ||||||
|  |          // Check PIL
 | ||||||
|  |          if (pil.size() >= 3) | ||||||
|  |          { | ||||||
|  |             auto pilPrefix = pil.substr(0, 3); | ||||||
|  |             auto windowIt  = pilLoadWindowStrings.find(pilPrefix); | ||||||
|  | 
 | ||||||
|  |             // Check Window
 | ||||||
|  |             if (windowIt != pilLoadWindowStrings.cend()) | ||||||
|  |             { | ||||||
|  |                const auto& productId   = it->productId_; | ||||||
|  |                const auto& windowStart = windowIt->second.first; | ||||||
|  |                const auto& windowEnd   = windowIt->second.second; | ||||||
|  | 
 | ||||||
|  |                if (windowStart <= productId && productId <= windowEnd) | ||||||
|  |                { | ||||||
|  |                   // Product matches, move it to the load list
 | ||||||
|  |                   loadListEntries.emplace_back(std::move(*it)); | ||||||
|  |                   it = mapIt->second.erase(it); | ||||||
|  |                   continue; | ||||||
|  |                } | ||||||
|  |             } | ||||||
|  |          } | ||||||
|  | 
 | ||||||
|  |          // Current iterator was not matched
 | ||||||
|  |          ++it; | ||||||
|  |       } | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    std::vector<std::shared_ptr<awips::TextProductFile>> products {}; | ||||||
|  | 
 | ||||||
|  |    // Load the load list
 | ||||||
|  |    if (!loadListEntries.empty()) | ||||||
|  |    { | ||||||
|  |       auto loadView = loadListEntries | | ||||||
|  |                       ranges::views::transform([](const auto& entry) | ||||||
|  |                                                { return entry.productId_; }); | ||||||
|  |       products = provider::IemApiProvider::LoadTextProducts(loadView); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    // Process loaded products
 | ||||||
|  |    for (auto& product : products) | ||||||
|  |    { | ||||||
|  |       const auto& messages = product->messages(); | ||||||
|  | 
 | ||||||
|  |       for (auto& message : messages) | ||||||
|  |       { | ||||||
|  |          HandleMessage(message, true); | ||||||
|  |       } | ||||||
|  |    } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void TextEventManager::Impl::PruneArchives() | ||||||
|  | { | ||||||
|  |    static constexpr std::size_t kMaxArchiveDates_ = 5; | ||||||
|  | 
 | ||||||
|  |    std::unordered_set<types::TextEventKey, | ||||||
|  |                       types::TextEventHash<types::TextEventKey>> | ||||||
|  |       eventKeysToKeep {}; | ||||||
|  |    std::unordered_set<types::TextEventKey, | ||||||
|  |                       types::TextEventHash<types::TextEventKey>> | ||||||
|  |       eventKeysToPrune {}; | ||||||
|  | 
 | ||||||
|  |    // Remove oldest dates from the archive
 | ||||||
|  |    while (archiveDates_.size() > kMaxArchiveDates_) | ||||||
|  |    { | ||||||
|  |       archiveDates_.pop_front(); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    const std::unique_lock archiveEventKeyLock {archiveEventKeyMutex_}; | ||||||
|  | 
 | ||||||
|  |    // If there are the same number of dates in archiveEventKeys_, archiveDates_
 | ||||||
|  |    // and unloadedProductMap_, there is nothing to prune
 | ||||||
|  |    if (archiveEventKeys_.size() == archiveDates_.size() && | ||||||
|  |        unloadedProductMap_.size() == archiveDates_.size()) | ||||||
|  |    { | ||||||
|  |       // Nothing to prune
 | ||||||
|  |       return; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    const std::unique_lock unloadedProductMapLock {unloadedProductMapMutex_}; | ||||||
|  | 
 | ||||||
|  |    for (auto it = archiveEventKeys_.begin(); it != archiveEventKeys_.end();) | ||||||
|  |    { | ||||||
|  |       const auto& date      = it->first; | ||||||
|  |       const auto& eventKeys = it->second; | ||||||
|  | 
 | ||||||
|  |       // If date is not in recent days map
 | ||||||
|  |       if (std::find(archiveDates_.cbegin(), archiveDates_.cend(), date) == | ||||||
|  |           archiveDates_.cend()) | ||||||
|  |       { | ||||||
|  |          // Prune these keys (unless they are in the eventKeysToKeep set)
 | ||||||
|  |          eventKeysToPrune.insert(eventKeys.begin(), eventKeys.end()); | ||||||
|  | 
 | ||||||
|  |          // The date is not in the list of recent dates, remove it
 | ||||||
|  |          it = archiveEventKeys_.erase(it); | ||||||
|  |       } | ||||||
|  |       else | ||||||
|  |       { | ||||||
|  |          // Make sure these keys don't get pruned
 | ||||||
|  |          eventKeysToKeep.insert(eventKeys.begin(), eventKeys.end()); | ||||||
|  | 
 | ||||||
|  |          // The date is recent, keep it
 | ||||||
|  |          ++it; | ||||||
|  |       } | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    for (auto it = unloadedProductMap_.begin(); it != unloadedProductMap_.end();) | ||||||
|  |    { | ||||||
|  |       const auto& date = it->first; | ||||||
|  | 
 | ||||||
|  |       // If date is not in recent days map
 | ||||||
|  |       if (std::find(archiveDates_.cbegin(), archiveDates_.cend(), date) == | ||||||
|  |           archiveDates_.cend()) | ||||||
|  |       { | ||||||
|  |          // The date is not in the list of recent dates, remove it
 | ||||||
|  |          it = unloadedProductMap_.erase(it); | ||||||
|  |       } | ||||||
|  |       else | ||||||
|  |       { | ||||||
|  |          // The date is recent, keep it
 | ||||||
|  |          ++it; | ||||||
|  |       } | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    // Remove elements from eventKeysToPrune if they are in eventKeysToKeep
 | ||||||
|  |    for (const auto& eventKey : eventKeysToKeep) | ||||||
|  |    { | ||||||
|  |       eventKeysToPrune.erase(eventKey); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    // Remove eventKeysToPrune from textEventMap
 | ||||||
|  |    for (const auto& eventKey : eventKeysToPrune) | ||||||
|  |    { | ||||||
|  |       textEventMap_.erase(eventKey); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    // If event keys were pruned, emit a signal
 | ||||||
|  |    if (!eventKeysToPrune.empty()) | ||||||
|  |    { | ||||||
|  |       logger_->debug("Pruned {} archive events", eventKeysToPrune.size()); | ||||||
|  | 
 | ||||||
|  |       Q_EMIT self_->AlertsRemoved(eventKeysToPrune); | ||||||
|    } |    } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -254,13 +672,31 @@ void TextEventManager::Impl::Refresh() | ||||||
|    std::shared_ptr<provider::WarningsProvider> warningsProvider = |    std::shared_ptr<provider::WarningsProvider> warningsProvider = | ||||||
|       warningsProvider_; |       warningsProvider_; | ||||||
| 
 | 
 | ||||||
|    // Update the file listing from the warnings provider
 |    // Load updated files from the warnings provider
 | ||||||
|    auto [newFiles, totalFiles] = warningsProvider->ListFiles(); |    // Start time should default to:
 | ||||||
|  |    // - 3 days of history for the first load
 | ||||||
|  |    // - 1 hour of history for subsequent loads
 | ||||||
|  |    // If the time jumps, we should attempt to load from no later than the
 | ||||||
|  |    // previous load time
 | ||||||
|  |    auto loadTime = | ||||||
|  |       std::chrono::floor<std::chrono::hours>(std::chrono::system_clock::now()); | ||||||
|  |    auto startTime = loadTime - loadHistoryDuration_; | ||||||
| 
 | 
 | ||||||
|    if (newFiles > 0) |    if (prevLoadTime_ != std::chrono::sys_time<std::chrono::hours> {}) | ||||||
|    { |    { | ||||||
|       // Load new files
 |       startTime = std::min(startTime, prevLoadTime_); | ||||||
|       auto updatedFiles = warningsProvider->LoadUpdatedFiles(); |    } | ||||||
|  | 
 | ||||||
|  |    if (archiveLimit_ == std::chrono::sys_days {}) | ||||||
|  |    { | ||||||
|  |       archiveLimit_ = std::chrono::ceil<std::chrono::days>(startTime); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    auto updatedFiles = warningsProvider->LoadUpdatedFiles(startTime); | ||||||
|  | 
 | ||||||
|  |    // Store the load time and reset the load history duration
 | ||||||
|  |    prevLoadTime_        = loadTime; | ||||||
|  |    loadHistoryDuration_ = kDefaultLoadHistoryDuration_; | ||||||
| 
 | 
 | ||||||
|    // Handle messages
 |    // Handle messages
 | ||||||
|    for (auto& file : updatedFiles) |    for (auto& file : updatedFiles) | ||||||
|  | @ -270,7 +706,6 @@ void TextEventManager::Impl::Refresh() | ||||||
|          HandleMessage(message); |          HandleMessage(message); | ||||||
|       } |       } | ||||||
|    } |    } | ||||||
|    } |  | ||||||
| 
 | 
 | ||||||
|    // Schedule another update in 15 seconds
 |    // Schedule another update in 15 seconds
 | ||||||
|    using namespace std::chrono; |    using namespace std::chrono; | ||||||
|  | @ -293,6 +728,19 @@ void TextEventManager::Impl::Refresh() | ||||||
|       }); |       }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | template<ranges::forward_range DateRange> | ||||||
|  |    requires std::same_as<ranges::range_value_t<DateRange>, | ||||||
|  |                          std::chrono::sys_days> | ||||||
|  | void TextEventManager::Impl::UpdateArchiveDates(DateRange dates) | ||||||
|  | { | ||||||
|  |    for (const auto& date : dates) | ||||||
|  |    { | ||||||
|  |       // Remove any existing occurrences of day, and add to the back of the list
 | ||||||
|  |       archiveDates_.remove(date); | ||||||
|  |       archiveDates_.push_back(date); | ||||||
|  |    } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| std::shared_ptr<TextEventManager> TextEventManager::Instance() | std::shared_ptr<TextEventManager> TextEventManager::Instance() | ||||||
| { | { | ||||||
|    static std::weak_ptr<TextEventManager> textEventManagerReference_ {}; |    static std::weak_ptr<TextEventManager> textEventManagerReference_ {}; | ||||||
|  | @ -312,6 +760,4 @@ std::shared_ptr<TextEventManager> TextEventManager::Instance() | ||||||
|    return textEventManager; |    return textEventManager; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| } // namespace manager
 | } // namespace scwx::qt::manager
 | ||||||
| } // namespace qt
 |  | ||||||
| } // namespace scwx
 |  | ||||||
|  |  | ||||||
|  | @ -3,9 +3,12 @@ | ||||||
| #include <scwx/awips/text_product_message.hpp> | #include <scwx/awips/text_product_message.hpp> | ||||||
| #include <scwx/qt/types/text_event_key.hpp> | #include <scwx/qt/types/text_event_key.hpp> | ||||||
| 
 | 
 | ||||||
|  | #include <chrono> | ||||||
| #include <memory> | #include <memory> | ||||||
| #include <string> | #include <string> | ||||||
|  | #include <unordered_set> | ||||||
| 
 | 
 | ||||||
|  | #include <boost/uuid/uuid.hpp> | ||||||
| #include <QObject> | #include <QObject> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx | ||||||
|  | @ -28,11 +31,18 @@ public: | ||||||
|    message_list(const types::TextEventKey& key) const; |    message_list(const types::TextEventKey& key) const; | ||||||
| 
 | 
 | ||||||
|    void LoadFile(const std::string& filename); |    void LoadFile(const std::string& filename); | ||||||
|  |    void SelectTime(std::chrono::system_clock::time_point dateTime); | ||||||
| 
 | 
 | ||||||
|    static std::shared_ptr<TextEventManager> Instance(); |    static std::shared_ptr<TextEventManager> Instance(); | ||||||
| 
 | 
 | ||||||
| signals: | signals: | ||||||
|    void AlertUpdated(const types::TextEventKey& key, size_t messageIndex); |    void AlertsRemoved( | ||||||
|  |       const std::unordered_set<types::TextEventKey, | ||||||
|  |                                types::TextEventHash<types::TextEventKey>>& | ||||||
|  |          keys); | ||||||
|  |    void AlertUpdated(const types::TextEventKey& key, | ||||||
|  |                      std::size_t                messageIndex, | ||||||
|  |                      boost::uuids::uuid         uuid); | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|    class Impl; |    class Impl; | ||||||
|  |  | ||||||
|  | @ -112,6 +112,11 @@ public: | ||||||
| TimelineManager::TimelineManager() : p(std::make_unique<Impl>(this)) {} | TimelineManager::TimelineManager() : p(std::make_unique<Impl>(this)) {} | ||||||
| TimelineManager::~TimelineManager() = default; | TimelineManager::~TimelineManager() = default; | ||||||
| 
 | 
 | ||||||
|  | std::chrono::system_clock::time_point TimelineManager::GetSelectedTime() const | ||||||
|  | { | ||||||
|  |    return p->selectedTime_; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void TimelineManager::SetMapCount(std::size_t mapCount) | void TimelineManager::SetMapCount(std::size_t mapCount) | ||||||
| { | { | ||||||
|    p->mapCount_ = mapCount; |    p->mapCount_ = mapCount; | ||||||
|  |  | ||||||
|  | @ -24,6 +24,8 @@ public: | ||||||
| 
 | 
 | ||||||
|    static std::shared_ptr<TimelineManager> Instance(); |    static std::shared_ptr<TimelineManager> Instance(); | ||||||
| 
 | 
 | ||||||
|  |    [[nodiscard]] std::chrono::system_clock::time_point GetSelectedTime() const; | ||||||
|  | 
 | ||||||
|    void SetMapCount(std::size_t mapCount); |    void SetMapCount(std::size_t mapCount); | ||||||
| 
 | 
 | ||||||
| public slots: | public slots: | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| #include <scwx/qt/manager/update_manager.hpp> | #include <scwx/qt/manager/update_manager.hpp> | ||||||
|  | #include <scwx/util/json.hpp> | ||||||
| #include <scwx/util/logger.hpp> | #include <scwx/util/logger.hpp> | ||||||
| 
 | 
 | ||||||
| #include <mutex> | #include <mutex> | ||||||
|  | @ -30,7 +31,6 @@ public: | ||||||
|    ~Impl() {} |    ~Impl() {} | ||||||
| 
 | 
 | ||||||
|    static std::string GetVersionString(const std::string& releaseName); |    static std::string GetVersionString(const std::string& releaseName); | ||||||
|    static boost::json::value ParseResponseText(const std::string& s); |  | ||||||
| 
 | 
 | ||||||
|    size_t PopulateReleases(); |    size_t PopulateReleases(); | ||||||
|    size_t AddReleases(const boost::json::value& json); |    size_t AddReleases(const boost::json::value& json); | ||||||
|  | @ -70,28 +70,6 @@ UpdateManager::Impl::GetVersionString(const std::string& releaseName) | ||||||
|    return versionString; |    return versionString; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| boost::json::value UpdateManager::Impl::ParseResponseText(const std::string& s) |  | ||||||
| { |  | ||||||
|    boost::json::stream_parser p; |  | ||||||
|    boost::system::error_code  ec; |  | ||||||
| 
 |  | ||||||
|    p.write(s, ec); |  | ||||||
|    if (ec) |  | ||||||
|    { |  | ||||||
|       logger_->warn("{}", ec.message()); |  | ||||||
|       return nullptr; |  | ||||||
|    } |  | ||||||
| 
 |  | ||||||
|    p.finish(ec); |  | ||||||
|    if (ec) |  | ||||||
|    { |  | ||||||
|       logger_->warn("{}", ec.message()); |  | ||||||
|       return nullptr; |  | ||||||
|    } |  | ||||||
| 
 |  | ||||||
|    return p.release(); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| bool UpdateManager::CheckForUpdates(const std::string& currentVersion) | bool UpdateManager::CheckForUpdates(const std::string& currentVersion) | ||||||
| { | { | ||||||
|    std::unique_lock lock(p->updateMutex_); |    std::unique_lock lock(p->updateMutex_); | ||||||
|  | @ -148,7 +126,7 @@ size_t UpdateManager::Impl::PopulateReleases() | ||||||
|       // Successful REST API query
 |       // Successful REST API query
 | ||||||
|       if (r.status_code == 200) |       if (r.status_code == 200) | ||||||
|       { |       { | ||||||
|          boost::json::value json = Impl::ParseResponseText(r.text); |          const boost::json::value json = util::json::ReadJsonString(r.text); | ||||||
|          if (json == nullptr) |          if (json == nullptr) | ||||||
|          { |          { | ||||||
|             logger_->warn("Response not JSON: {}", r.header["content-type"]); |             logger_->warn("Response not JSON: {}", r.header["content-type"]); | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ | ||||||
| #include <chrono> | #include <chrono> | ||||||
| #include <mutex> | #include <mutex> | ||||||
| #include <ranges> | #include <ranges> | ||||||
|  | #include <set> | ||||||
| #include <unordered_map> | #include <unordered_map> | ||||||
| #include <unordered_set> | #include <unordered_set> | ||||||
| 
 | 
 | ||||||
|  | @ -19,12 +20,9 @@ | ||||||
| #include <boost/container/stable_vector.hpp> | #include <boost/container/stable_vector.hpp> | ||||||
| #include <boost/container_hash/hash.hpp> | #include <boost/container_hash/hash.hpp> | ||||||
| #include <QEvent> | #include <QEvent> | ||||||
|  | #include <utility> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx::qt::map | ||||||
| { |  | ||||||
| namespace qt |  | ||||||
| { |  | ||||||
| namespace map |  | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| static const std::string logPrefix_ = "scwx::qt::map::alert_layer"; | static const std::string logPrefix_ = "scwx::qt::map::alert_layer"; | ||||||
|  | @ -46,6 +44,8 @@ static bool IsAlertActive(const std::shared_ptr<const awips::Segment>& segment); | ||||||
| class AlertLayerHandler : public QObject | class AlertLayerHandler : public QObject | ||||||
| { | { | ||||||
|    Q_OBJECT |    Q_OBJECT | ||||||
|  |    Q_DISABLE_COPY_MOVE(AlertLayerHandler) | ||||||
|  | 
 | ||||||
| public: | public: | ||||||
|    struct SegmentRecord |    struct SegmentRecord | ||||||
|    { |    { | ||||||
|  | @ -57,10 +57,10 @@ public: | ||||||
| 
 | 
 | ||||||
|       SegmentRecord( |       SegmentRecord( | ||||||
|          const std::shared_ptr<const awips::Segment>&            segment, |          const std::shared_ptr<const awips::Segment>&            segment, | ||||||
|          const types::TextEventKey&                              key, |          types::TextEventKey                                     key, | ||||||
|          const std::shared_ptr<const awips::TextProductMessage>& message) : |          const std::shared_ptr<const awips::TextProductMessage>& message) : | ||||||
|           segment_ {segment}, |           segment_ {segment}, | ||||||
|           key_ {key}, |           key_ {std::move(key)}, | ||||||
|           message_ {message}, |           message_ {message}, | ||||||
|           segmentBegin_ {segment->event_begin()}, |           segmentBegin_ {segment->event_begin()}, | ||||||
|           segmentEnd_ {segment->event_end()} |           segmentEnd_ {segment->event_end()} | ||||||
|  | @ -73,8 +73,11 @@ public: | ||||||
|       connect(textEventManager_.get(), |       connect(textEventManager_.get(), | ||||||
|               &manager::TextEventManager::AlertUpdated, |               &manager::TextEventManager::AlertUpdated, | ||||||
|               this, |               this, | ||||||
|               [this](const types::TextEventKey& key, std::size_t messageIndex) |               &AlertLayerHandler::HandleAlert); | ||||||
|               { HandleAlert(key, messageIndex); }); |       connect(textEventManager_.get(), | ||||||
|  |               &manager::TextEventManager::AlertsRemoved, | ||||||
|  |               this, | ||||||
|  |               &AlertLayerHandler::HandleAlertsRemoved); | ||||||
|    } |    } | ||||||
|    ~AlertLayerHandler() |    ~AlertLayerHandler() | ||||||
|    { |    { | ||||||
|  | @ -95,7 +98,13 @@ public: | ||||||
|       types::TextEventHash<types::TextEventKey>> |       types::TextEventHash<types::TextEventKey>> | ||||||
|       segmentsByKey_ {}; |       segmentsByKey_ {}; | ||||||
| 
 | 
 | ||||||
|    void HandleAlert(const types::TextEventKey& key, size_t messageIndex); |    void HandleAlert(const types::TextEventKey& key, | ||||||
|  |                     size_t                     messageIndex, | ||||||
|  |                     boost::uuids::uuid         uuid); | ||||||
|  |    void HandleAlertsRemoved( | ||||||
|  |       const std::unordered_set<types::TextEventKey, | ||||||
|  |                                types::TextEventHash<types::TextEventKey>>& | ||||||
|  |          keys); | ||||||
| 
 | 
 | ||||||
|    static AlertLayerHandler& Instance(); |    static AlertLayerHandler& Instance(); | ||||||
| 
 | 
 | ||||||
|  | @ -108,6 +117,7 @@ signals: | ||||||
|    void AlertAdded(const std::shared_ptr<SegmentRecord>& segmentRecord, |    void AlertAdded(const std::shared_ptr<SegmentRecord>& segmentRecord, | ||||||
|                    awips::Phenomenon                     phenomenon); |                    awips::Phenomenon                     phenomenon); | ||||||
|    void AlertUpdated(const std::shared_ptr<SegmentRecord>& segmentRecord); |    void AlertUpdated(const std::shared_ptr<SegmentRecord>& segmentRecord); | ||||||
|  |    void AlertsRemoved(awips::Phenomenon phenomenon); | ||||||
|    void AlertsUpdated(awips::Phenomenon phenomenon, bool alertActive); |    void AlertsUpdated(awips::Phenomenon phenomenon, bool alertActive); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -151,6 +161,11 @@ public: | ||||||
|       std::unique_lock lock(linesMutex_); |       std::unique_lock lock(linesMutex_); | ||||||
|    }; |    }; | ||||||
| 
 | 
 | ||||||
|  |    Impl(const Impl&)             = delete; | ||||||
|  |    Impl& operator=(const Impl&)  = delete; | ||||||
|  |    Impl(const Impl&&)            = delete; | ||||||
|  |    Impl& operator=(const Impl&&) = delete; | ||||||
|  | 
 | ||||||
|    void AddAlert( |    void AddAlert( | ||||||
|       const std::shared_ptr<AlertLayerHandler::SegmentRecord>& segmentRecord); |       const std::shared_ptr<AlertLayerHandler::SegmentRecord>& segmentRecord); | ||||||
|    void UpdateAlert( |    void UpdateAlert( | ||||||
|  | @ -172,20 +187,22 @@ public: | ||||||
|                 std::shared_ptr<gl::draw::GeoLineDrawItem>& di, |                 std::shared_ptr<gl::draw::GeoLineDrawItem>& di, | ||||||
|                 const common::Coordinate&                   p1, |                 const common::Coordinate&                   p1, | ||||||
|                 const common::Coordinate&                   p2, |                 const common::Coordinate&                   p2, | ||||||
|                 boost::gil::rgba32f_pixel_t                 color, |                 const boost::gil::rgba32f_pixel_t&          color, | ||||||
|                 float                                       width, |                 float                                       width, | ||||||
|                 std::chrono::system_clock::time_point       startTime, |                 std::chrono::system_clock::time_point       startTime, | ||||||
|                 std::chrono::system_clock::time_point       endTime, |                 std::chrono::system_clock::time_point       endTime, | ||||||
|                 bool                                        enableHover); |                 bool                                        enableHover); | ||||||
|    void AddLines(std::shared_ptr<gl::draw::GeoLines>&   geoLines, |    void AddLines(std::shared_ptr<gl::draw::GeoLines>&   geoLines, | ||||||
|                  const std::vector<common::Coordinate>& coordinates, |                  const std::vector<common::Coordinate>& coordinates, | ||||||
|                  boost::gil::rgba32f_pixel_t            color, |                  const boost::gil::rgba32f_pixel_t&     color, | ||||||
|                  float                                  width, |                  float                                  width, | ||||||
|                  std::chrono::system_clock::time_point  startTime, |                  std::chrono::system_clock::time_point  startTime, | ||||||
|                  std::chrono::system_clock::time_point  endTime, |                  std::chrono::system_clock::time_point  endTime, | ||||||
|                  bool                                   enableHover, |                  bool                                   enableHover, | ||||||
|                  boost::container::stable_vector< |                  boost::container::stable_vector< | ||||||
|                     std::shared_ptr<gl::draw::GeoLineDrawItem>>& drawItems); |                     std::shared_ptr<gl::draw::GeoLineDrawItem>>& drawItems); | ||||||
|  |    void PopulateLines(bool alertActive); | ||||||
|  |    void RepopulateLines(); | ||||||
|    void UpdateLines(); |    void UpdateLines(); | ||||||
| 
 | 
 | ||||||
|    static LineData CreateLineData(const settings::LineSettings& lineSettings); |    static LineData CreateLineData(const settings::LineSettings& lineSettings); | ||||||
|  | @ -201,6 +218,7 @@ public: | ||||||
|    const awips::ibw::ImpactBasedWarningInfo& ibw_; |    const awips::ibw::ImpactBasedWarningInfo& ibw_; | ||||||
| 
 | 
 | ||||||
|    std::unique_ptr<QObject> receiver_ {std::make_unique<QObject>()}; |    std::unique_ptr<QObject> receiver_ {std::make_unique<QObject>()}; | ||||||
|  |    std::mutex               receiverMutex_ {}; | ||||||
| 
 | 
 | ||||||
|    std::unordered_map<bool, std::shared_ptr<gl::draw::GeoLines>> geoLines_; |    std::unordered_map<bool, std::shared_ptr<gl::draw::GeoLines>> geoLines_; | ||||||
| 
 | 
 | ||||||
|  | @ -227,7 +245,7 @@ public: | ||||||
|    std::vector<boost::signals2::scoped_connection> connections_ {}; |    std::vector<boost::signals2::scoped_connection> connections_ {}; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| AlertLayer::AlertLayer(std::shared_ptr<MapContext> context, | AlertLayer::AlertLayer(const std::shared_ptr<MapContext>& context, | ||||||
|                        awips::Phenomenon                  phenomenon) : |                        awips::Phenomenon                  phenomenon) : | ||||||
|     DrawLayer( |     DrawLayer( | ||||||
|        context, |        context, | ||||||
|  | @ -264,28 +282,15 @@ void AlertLayer::Initialize() | ||||||
| 
 | 
 | ||||||
|    auto& alertLayerHandler = AlertLayerHandler::Instance(); |    auto& alertLayerHandler = AlertLayerHandler::Instance(); | ||||||
| 
 | 
 | ||||||
|  |    p->selectedTime_ = manager::TimelineManager::Instance()->GetSelectedTime(); | ||||||
|  | 
 | ||||||
|    // Take a shared lock to prevent handling additional alerts while populating
 |    // Take a shared lock to prevent handling additional alerts while populating
 | ||||||
|    // initial lists
 |    // initial lists
 | ||||||
|    std::shared_lock lock {alertLayerHandler.alertMutex_}; |    std::shared_lock lock {alertLayerHandler.alertMutex_}; | ||||||
| 
 | 
 | ||||||
|    for (auto alertActive : {false, true}) |    for (auto alertActive : {false, true}) | ||||||
|    { |    { | ||||||
|       auto& geoLines = p->geoLines_.at(alertActive); |       p->PopulateLines(alertActive); | ||||||
| 
 |  | ||||||
|       geoLines->StartLines(); |  | ||||||
| 
 |  | ||||||
|       // Populate initial segments
 |  | ||||||
|       auto segmentsIt = |  | ||||||
|          alertLayerHandler.segmentsByType_.find({p->phenomenon_, alertActive}); |  | ||||||
|       if (segmentsIt != alertLayerHandler.segmentsByType_.cend()) |  | ||||||
|       { |  | ||||||
|          for (auto& segment : segmentsIt->second) |  | ||||||
|          { |  | ||||||
|             p->AddAlert(segment); |  | ||||||
|          } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       geoLines->FinishLines(); |  | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|    p->ConnectAlertHandlerSignals(); |    p->ConnectAlertHandlerSignals(); | ||||||
|  | @ -322,7 +327,8 @@ bool IsAlertActive(const std::shared_ptr<const awips::Segment>& segment) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, | void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, | ||||||
|                                     size_t                     messageIndex) |                                     size_t                     messageIndex, | ||||||
|  |                                     boost::uuids::uuid         uuid) | ||||||
| { | { | ||||||
|    logger_->trace("HandleAlert: {}", key.ToString()); |    logger_->trace("HandleAlert: {}", key.ToString()); | ||||||
| 
 | 
 | ||||||
|  | @ -330,7 +336,28 @@ void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, | ||||||
|                       AlertTypeHash<std::pair<awips::Phenomenon, bool>>> |                       AlertTypeHash<std::pair<awips::Phenomenon, bool>>> | ||||||
|       alertsUpdated {}; |       alertsUpdated {}; | ||||||
| 
 | 
 | ||||||
|    auto message = textEventManager_->message_list(key).at(messageIndex); |    const auto& messageList = textEventManager_->message_list(key); | ||||||
|  | 
 | ||||||
|  |    // Find message by UUID instead of index, as the message index could have
 | ||||||
|  |    // changed between the signal being emitted and the handler being called
 | ||||||
|  |    auto messageIt = std::find_if(messageList.cbegin(), | ||||||
|  |                                  messageList.cend(), | ||||||
|  |                                  [&uuid](const auto& message) | ||||||
|  |                                  { return uuid == message->uuid(); }); | ||||||
|  | 
 | ||||||
|  |    if (messageIt == messageList.cend()) | ||||||
|  |    { | ||||||
|  |       logger_->warn( | ||||||
|  |          "Could not find alert uuid: {} ({})", key.ToString(), messageIndex); | ||||||
|  |       return; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    auto& message       = *messageIt; | ||||||
|  |    auto  nextMessageIt = std::next(messageIt); | ||||||
|  | 
 | ||||||
|  |    // Store the current message index
 | ||||||
|  |    messageIndex = | ||||||
|  |       static_cast<std::size_t>(std::distance(messageList.cbegin(), messageIt)); | ||||||
| 
 | 
 | ||||||
|    // Determine start time for first segment
 |    // Determine start time for first segment
 | ||||||
|    std::chrono::system_clock::time_point segmentBegin {}; |    std::chrono::system_clock::time_point segmentBegin {}; | ||||||
|  | @ -339,14 +366,31 @@ void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, | ||||||
|       segmentBegin = message->segment(0)->event_begin(); |       segmentBegin = message->segment(0)->event_begin(); | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|  |    // Determine the start time for the first segment of the next message
 | ||||||
|  |    std::optional<std::chrono::system_clock::time_point> nextMessageBegin {}; | ||||||
|  |    if (nextMessageIt != messageList.cend()) | ||||||
|  |    { | ||||||
|  |       nextMessageBegin = | ||||||
|  |          (*nextMessageIt) | ||||||
|  |             ->wmo_header() | ||||||
|  |             ->GetDateTime((*nextMessageIt)->segment(0)->event_begin()); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|    // Take a unique mutex before modifying segments
 |    // Take a unique mutex before modifying segments
 | ||||||
|    std::unique_lock lock {alertMutex_}; |    std::unique_lock lock {alertMutex_}; | ||||||
| 
 | 
 | ||||||
|    // Update any existing segments with new end time
 |    // Update any existing earlier segments with new end time
 | ||||||
|    auto& segmentsForKey = segmentsByKey_[key]; |    auto& segmentsForKey = segmentsByKey_[key]; | ||||||
|    for (auto& segmentRecord : segmentsForKey) |    for (auto& segmentRecord : segmentsForKey) | ||||||
|    { |    { | ||||||
|       if (segmentRecord->segmentEnd_ > segmentBegin) |       // Determine if the segment is earlier than the current message
 | ||||||
|  |       auto it = std::find( | ||||||
|  |          messageList.cbegin(), messageList.cend(), segmentRecord->message_); | ||||||
|  |       auto segmentIndex = | ||||||
|  |          static_cast<std::size_t>(std::distance(messageList.cbegin(), it)); | ||||||
|  | 
 | ||||||
|  |       if (segmentIndex < messageIndex && | ||||||
|  |           segmentRecord->segmentEnd_ > segmentBegin) | ||||||
|       { |       { | ||||||
|          segmentRecord->segmentEnd_ = segmentBegin; |          segmentRecord->segmentEnd_ = segmentBegin; | ||||||
| 
 | 
 | ||||||
|  | @ -373,6 +417,14 @@ void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, | ||||||
|       std::shared_ptr<SegmentRecord> segmentRecord = |       std::shared_ptr<SegmentRecord> segmentRecord = | ||||||
|          std::make_shared<SegmentRecord>(segment, key, message); |          std::make_shared<SegmentRecord>(segment, key, message); | ||||||
| 
 | 
 | ||||||
|  |       // Update segment end time to be no later than the begin time of the next
 | ||||||
|  |       // message (if present)
 | ||||||
|  |       if (nextMessageBegin.has_value() && | ||||||
|  |           segmentRecord->segmentEnd_ > nextMessageBegin) | ||||||
|  |       { | ||||||
|  |          segmentRecord->segmentEnd_ = nextMessageBegin.value(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       segmentsForKey.push_back(segmentRecord); |       segmentsForKey.push_back(segmentRecord); | ||||||
|       segmentsForType.push_back(segmentRecord); |       segmentsForType.push_back(segmentRecord); | ||||||
| 
 | 
 | ||||||
|  | @ -391,6 +443,63 @@ void AlertLayerHandler::HandleAlert(const types::TextEventKey& key, | ||||||
|    } |    } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void AlertLayerHandler::HandleAlertsRemoved( | ||||||
|  |    const std::unordered_set<types::TextEventKey, | ||||||
|  |                             types::TextEventHash<types::TextEventKey>>& keys) | ||||||
|  | { | ||||||
|  |    logger_->trace("HandleAlertsRemoved: {} keys", keys.size()); | ||||||
|  | 
 | ||||||
|  |    std::set<awips::Phenomenon> alertsRemoved {}; | ||||||
|  | 
 | ||||||
|  |    // Take a unique lock before modifying segments
 | ||||||
|  |    std::unique_lock lock {alertMutex_}; | ||||||
|  | 
 | ||||||
|  |    for (const auto& key : keys) | ||||||
|  |    { | ||||||
|  |       // Remove segments associated with the key
 | ||||||
|  |       auto segmentsIt = segmentsByKey_.find(key); | ||||||
|  |       if (segmentsIt != segmentsByKey_.end()) | ||||||
|  |       { | ||||||
|  |          for (const auto& segmentRecord : segmentsIt->second) | ||||||
|  |          { | ||||||
|  |             auto&      segment     = segmentRecord->segment_; | ||||||
|  |             const bool alertActive = IsAlertActive(segment); | ||||||
|  | 
 | ||||||
|  |             // Remove from segmentsByType_
 | ||||||
|  |             auto typeIt = segmentsByType_.find({key.phenomenon_, alertActive}); | ||||||
|  |             if (typeIt != segmentsByType_.end()) | ||||||
|  |             { | ||||||
|  |                auto& segmentsForType = typeIt->second; | ||||||
|  |                segmentsForType.erase(std::remove(segmentsForType.begin(), | ||||||
|  |                                                  segmentsForType.end(), | ||||||
|  |                                                  segmentRecord), | ||||||
|  |                                      segmentsForType.end()); | ||||||
|  | 
 | ||||||
|  |                // If no segments remain for this type, erase the entry
 | ||||||
|  |                if (segmentsForType.empty()) | ||||||
|  |                { | ||||||
|  |                   segmentsByType_.erase(typeIt); | ||||||
|  |                } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             alertsRemoved.emplace(key.phenomenon_); | ||||||
|  |          } | ||||||
|  | 
 | ||||||
|  |          // Remove the key from segmentsByKey_
 | ||||||
|  |          segmentsByKey_.erase(segmentsIt); | ||||||
|  |       } | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    // Release the lock after completing segment updates
 | ||||||
|  |    lock.unlock(); | ||||||
|  | 
 | ||||||
|  |    // Emit signal to notify that alerts have been removed
 | ||||||
|  |    for (auto& alert : alertsRemoved) | ||||||
|  |    { | ||||||
|  |       Q_EMIT AlertsRemoved(alert); | ||||||
|  |    } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void AlertLayer::Impl::ConnectAlertHandlerSignals() | void AlertLayer::Impl::ConnectAlertHandlerSignals() | ||||||
| { | { | ||||||
|    auto& alertLayerHandler = AlertLayerHandler::Instance(); |    auto& alertLayerHandler = AlertLayerHandler::Instance(); | ||||||
|  | @ -405,6 +514,9 @@ void AlertLayer::Impl::ConnectAlertHandlerSignals() | ||||||
|       { |       { | ||||||
|          if (phenomenon == phenomenon_) |          if (phenomenon == phenomenon_) | ||||||
|          { |          { | ||||||
|  |             // Only process one signal at a time
 | ||||||
|  |             const std::unique_lock lock {receiverMutex_}; | ||||||
|  | 
 | ||||||
|             AddAlert(segmentRecord); |             AddAlert(segmentRecord); | ||||||
|          } |          } | ||||||
|       }); |       }); | ||||||
|  | @ -417,9 +529,27 @@ void AlertLayer::Impl::ConnectAlertHandlerSignals() | ||||||
|       { |       { | ||||||
|          if (segmentRecord->key_.phenomenon_ == phenomenon_) |          if (segmentRecord->key_.phenomenon_ == phenomenon_) | ||||||
|          { |          { | ||||||
|  |             // Only process one signal at a time
 | ||||||
|  |             const std::unique_lock lock {receiverMutex_}; | ||||||
|  | 
 | ||||||
|             UpdateAlert(segmentRecord); |             UpdateAlert(segmentRecord); | ||||||
|          } |          } | ||||||
|       }); |       }); | ||||||
|  |    QObject::connect(&alertLayerHandler, | ||||||
|  |                     &AlertLayerHandler::AlertsRemoved, | ||||||
|  |                     receiver_.get(), | ||||||
|  |                     [this](awips::Phenomenon phenomenon) | ||||||
|  |                     { | ||||||
|  |                        if (phenomenon == phenomenon_) | ||||||
|  |                        { | ||||||
|  |                           // Only process one signal at a time
 | ||||||
|  |                           const std::unique_lock lock {receiverMutex_}; | ||||||
|  | 
 | ||||||
|  |                           // Re-populate the lines if multiple alerts were
 | ||||||
|  |                           // removed
 | ||||||
|  |                           RepopulateLines(); | ||||||
|  |                        } | ||||||
|  |                     }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void AlertLayer::Impl::ConnectSignals() | void AlertLayer::Impl::ConnectSignals() | ||||||
|  | @ -500,9 +630,9 @@ void AlertLayer::Impl::AddAlert( | ||||||
|    // If draw items were added
 |    // If draw items were added
 | ||||||
|    if (drawItems.second) |    if (drawItems.second) | ||||||
|    { |    { | ||||||
|       const float borderWidth    = lineData.borderWidth_; |       const auto borderWidth    = static_cast<float>(lineData.borderWidth_); | ||||||
|       const float highlightWidth = lineData.highlightWidth_; |       const auto highlightWidth = static_cast<float>(lineData.highlightWidth_); | ||||||
|       const float lineWidth      = lineData.lineWidth_; |       const auto lineWidth      = static_cast<float>(lineData.lineWidth_); | ||||||
| 
 | 
 | ||||||
|       const float totalHighlightWidth = lineWidth + (highlightWidth * 2.0f); |       const float totalHighlightWidth = lineWidth + (highlightWidth * 2.0f); | ||||||
|       const float totalBorderWidth = totalHighlightWidth + (borderWidth * 2.0f); |       const float totalBorderWidth = totalHighlightWidth + (borderWidth * 2.0f); | ||||||
|  | @ -547,6 +677,8 @@ void AlertLayer::Impl::AddAlert( | ||||||
|                lineHover, |                lineHover, | ||||||
|                drawItems.first->second); |                drawItems.first->second); | ||||||
|    } |    } | ||||||
|  | 
 | ||||||
|  |    Q_EMIT self_->NeedsRendering(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void AlertLayer::Impl::UpdateAlert( | void AlertLayer::Impl::UpdateAlert( | ||||||
|  | @ -570,12 +702,14 @@ void AlertLayer::Impl::UpdateAlert( | ||||||
|          geoLines->SetLineEndTime(line, segmentRecord->segmentEnd_); |          geoLines->SetLineEndTime(line, segmentRecord->segmentEnd_); | ||||||
|       } |       } | ||||||
|    } |    } | ||||||
|  | 
 | ||||||
|  |    Q_EMIT self_->NeedsRendering(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void AlertLayer::Impl::AddLines( | void AlertLayer::Impl::AddLines( | ||||||
|    std::shared_ptr<gl::draw::GeoLines>&   geoLines, |    std::shared_ptr<gl::draw::GeoLines>&   geoLines, | ||||||
|    const std::vector<common::Coordinate>& coordinates, |    const std::vector<common::Coordinate>& coordinates, | ||||||
|    boost::gil::rgba32f_pixel_t            color, |    const boost::gil::rgba32f_pixel_t&     color, | ||||||
|    float                                  width, |    float                                  width, | ||||||
|    std::chrono::system_clock::time_point  startTime, |    std::chrono::system_clock::time_point  startTime, | ||||||
|    std::chrono::system_clock::time_point  endTime, |    std::chrono::system_clock::time_point  endTime, | ||||||
|  | @ -615,14 +749,17 @@ void AlertLayer::Impl::AddLine(std::shared_ptr<gl::draw::GeoLines>& geoLines, | ||||||
|                                std::shared_ptr<gl::draw::GeoLineDrawItem>& di, |                                std::shared_ptr<gl::draw::GeoLineDrawItem>& di, | ||||||
|                                const common::Coordinate&                   p1, |                                const common::Coordinate&                   p1, | ||||||
|                                const common::Coordinate&                   p2, |                                const common::Coordinate&                   p2, | ||||||
|                                boost::gil::rgba32f_pixel_t           color, |                                const boost::gil::rgba32f_pixel_t&    color, | ||||||
|                                float                                 width, |                                float                                 width, | ||||||
|                                std::chrono::system_clock::time_point startTime, |                                std::chrono::system_clock::time_point startTime, | ||||||
|                                std::chrono::system_clock::time_point endTime, |                                std::chrono::system_clock::time_point endTime, | ||||||
|                                bool enableHover) |                                bool enableHover) | ||||||
| { | { | ||||||
|    geoLines->SetLineLocation( |    geoLines->SetLineLocation(di, | ||||||
|       di, p1.latitude_, p1.longitude_, p2.latitude_, p2.longitude_); |                              static_cast<float>(p1.latitude_), | ||||||
|  |                              static_cast<float>(p1.longitude_), | ||||||
|  |                              static_cast<float>(p2.latitude_), | ||||||
|  |                              static_cast<float>(p2.longitude_)); | ||||||
|    geoLines->SetLineModulate(di, color); |    geoLines->SetLineModulate(di, color); | ||||||
|    geoLines->SetLineWidth(di, width); |    geoLines->SetLineWidth(di, width); | ||||||
|    geoLines->SetLineStartTime(di, startTime); |    geoLines->SetLineStartTime(di, startTime); | ||||||
|  | @ -647,6 +784,46 @@ void AlertLayer::Impl::AddLine(std::shared_ptr<gl::draw::GeoLines>& geoLines, | ||||||
|    } |    } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void AlertLayer::Impl::PopulateLines(bool alertActive) | ||||||
|  | { | ||||||
|  |    auto& alertLayerHandler = AlertLayerHandler::Instance(); | ||||||
|  |    auto& geoLines          = geoLines_.at(alertActive); | ||||||
|  | 
 | ||||||
|  |    geoLines->StartLines(); | ||||||
|  | 
 | ||||||
|  |    // Populate initial segments
 | ||||||
|  |    auto segmentsIt = | ||||||
|  |       alertLayerHandler.segmentsByType_.find({phenomenon_, alertActive}); | ||||||
|  |    if (segmentsIt != alertLayerHandler.segmentsByType_.cend()) | ||||||
|  |    { | ||||||
|  |       for (auto& segment : segmentsIt->second) | ||||||
|  |       { | ||||||
|  |          AddAlert(segment); | ||||||
|  |       } | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    geoLines->FinishLines(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void AlertLayer::Impl::RepopulateLines() | ||||||
|  | { | ||||||
|  |    auto& alertLayerHandler = AlertLayerHandler::Instance(); | ||||||
|  | 
 | ||||||
|  |    // Take a shared lock to prevent handling additional alerts while populating
 | ||||||
|  |    // initial lists
 | ||||||
|  |    const std::shared_lock alertLock {alertLayerHandler.alertMutex_}; | ||||||
|  | 
 | ||||||
|  |    linesBySegment_.clear(); | ||||||
|  |    segmentsByLine_.clear(); | ||||||
|  | 
 | ||||||
|  |    for (auto alertActive : {false, true}) | ||||||
|  |    { | ||||||
|  |       PopulateLines(alertActive); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    Q_EMIT self_->NeedsRendering(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void AlertLayer::Impl::UpdateLines() | void AlertLayer::Impl::UpdateLines() | ||||||
| { | { | ||||||
|    std::unique_lock lock {linesMutex_}; |    std::unique_lock lock {linesMutex_}; | ||||||
|  | @ -660,9 +837,9 @@ void AlertLayer::Impl::UpdateLines() | ||||||
|       auto& lineData         = GetLineData(segment, alertActive); |       auto& lineData         = GetLineData(segment, alertActive); | ||||||
|       auto& geoLines         = geoLines_.at(alertActive); |       auto& geoLines         = geoLines_.at(alertActive); | ||||||
| 
 | 
 | ||||||
|       const float borderWidth    = lineData.borderWidth_; |       const auto borderWidth    = static_cast<float>(lineData.borderWidth_); | ||||||
|       const float highlightWidth = lineData.highlightWidth_; |       const auto highlightWidth = static_cast<float>(lineData.highlightWidth_); | ||||||
|       const float lineWidth      = lineData.lineWidth_; |       const auto lineWidth      = static_cast<float>(lineData.lineWidth_); | ||||||
| 
 | 
 | ||||||
|       const float totalHighlightWidth = lineWidth + (highlightWidth * 2.0f); |       const float totalHighlightWidth = lineWidth + (highlightWidth * 2.0f); | ||||||
|       const float totalBorderWidth = totalHighlightWidth + (borderWidth * 2.0f); |       const float totalBorderWidth = totalHighlightWidth + (borderWidth * 2.0f); | ||||||
|  | @ -837,8 +1014,6 @@ size_t AlertTypeHash<std::pair<awips::Phenomenon, bool>>::operator()( | ||||||
|    return seed; |    return seed; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| } // namespace map
 | } // namespace scwx::qt::map
 | ||||||
| } // namespace qt
 |  | ||||||
| } // namespace scwx
 |  | ||||||
| 
 | 
 | ||||||
| #include "alert_layer.moc" | #include "alert_layer.moc" | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ class AlertLayer : public DrawLayer | ||||||
|    Q_DISABLE_COPY_MOVE(AlertLayer) |    Q_DISABLE_COPY_MOVE(AlertLayer) | ||||||
| 
 | 
 | ||||||
| public: | public: | ||||||
|    explicit AlertLayer(std::shared_ptr<MapContext> context, |    explicit AlertLayer(const std::shared_ptr<MapContext>& context, | ||||||
|                        scwx::awips::Phenomenon            phenomenon); |                        scwx::awips::Phenomenon            phenomenon); | ||||||
|    ~AlertLayer(); |    ~AlertLayer(); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -122,6 +122,8 @@ void PlacefileLayer::Initialize() | ||||||
|    logger_->debug("Initialize()"); |    logger_->debug("Initialize()"); | ||||||
| 
 | 
 | ||||||
|    DrawLayer::Initialize(); |    DrawLayer::Initialize(); | ||||||
|  | 
 | ||||||
|  |    p->selectedTime_ = manager::TimelineManager::Instance()->GetSelectedTime(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void PlacefileLayer::Render( | void PlacefileLayer::Render( | ||||||
|  |  | ||||||
|  | @ -10,16 +10,10 @@ | ||||||
| #include <scwx/util/strings.hpp> | #include <scwx/util/strings.hpp> | ||||||
| #include <scwx/util/time.hpp> | #include <scwx/util/time.hpp> | ||||||
| 
 | 
 | ||||||
| #include <format> |  | ||||||
| 
 |  | ||||||
| #include <QApplication> | #include <QApplication> | ||||||
| #include <QFontMetrics> | #include <QFontMetrics> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx::qt::model | ||||||
| { |  | ||||||
| namespace qt |  | ||||||
| { |  | ||||||
| namespace model |  | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| static const std::string logPrefix_ = "scwx::qt::model::alert_model"; | static const std::string logPrefix_ = "scwx::qt::model::alert_model"; | ||||||
|  | @ -329,16 +323,45 @@ AlertModel::headerData(int section, Qt::Orientation orientation, int role) const | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void AlertModel::HandleAlert(const types::TextEventKey& alertKey, | void AlertModel::HandleAlert(const types::TextEventKey& alertKey, | ||||||
|                              size_t                     messageIndex) |                              std::size_t                messageIndex, | ||||||
|  |                              boost::uuids::uuid         uuid) | ||||||
| { | { | ||||||
|    logger_->trace("Handle alert: {}", alertKey.ToString()); |    logger_->trace("Handle alert: {}", alertKey.ToString()); | ||||||
| 
 | 
 | ||||||
|    double distanceInMeters; |    double distanceInMeters; | ||||||
| 
 | 
 | ||||||
|  |    const auto& alertMessages = p->textEventManager_->message_list(alertKey); | ||||||
|  | 
 | ||||||
|  |    // Find message by UUID instead of index, as the message index could have
 | ||||||
|  |    // changed between the signal being emitted and the handler being called
 | ||||||
|  |    auto messageIt = std::find_if(alertMessages.cbegin(), | ||||||
|  |                                  alertMessages.cend(), | ||||||
|  |                                  [&uuid](const auto& message) | ||||||
|  |                                  { return uuid == message->uuid(); }); | ||||||
|  | 
 | ||||||
|  |    if (messageIt == alertMessages.cend()) | ||||||
|  |    { | ||||||
|  |       logger_->warn("Could not find alert uuid: {} ({})", | ||||||
|  |                     alertKey.ToString(), | ||||||
|  |                     messageIndex); | ||||||
|  |       return; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    auto& message = *messageIt; | ||||||
|  | 
 | ||||||
|  |    // Store the current message index
 | ||||||
|  |    messageIndex = static_cast<std::size_t>( | ||||||
|  |       std::distance(alertMessages.cbegin(), messageIt)); | ||||||
|  | 
 | ||||||
|  |    // Skip alert if this is not the most recent message
 | ||||||
|  |    if (messageIndex + 1 < alertMessages.size()) | ||||||
|  |    { | ||||||
|  |       return; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|    // Get the most recent segment for the event
 |    // Get the most recent segment for the event
 | ||||||
|    auto alertMessages = p->textEventManager_->message_list(alertKey); |    const std::shared_ptr<const awips::Segment> alertSegment = | ||||||
|    std::shared_ptr<const awips::Segment> alertSegment = |       message->segments().back(); | ||||||
|       alertMessages[messageIndex]->segments().back(); |  | ||||||
| 
 | 
 | ||||||
|    p->observedMap_.insert_or_assign(alertKey, alertSegment->observed_); |    p->observedMap_.insert_or_assign(alertKey, alertSegment->observed_); | ||||||
|    p->threatCategoryMap_.insert_or_assign(alertKey, |    p->threatCategoryMap_.insert_or_assign(alertKey, | ||||||
|  | @ -386,6 +409,36 @@ void AlertModel::HandleAlert(const types::TextEventKey& alertKey, | ||||||
|    } |    } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void AlertModel::HandleAlertsRemoved( | ||||||
|  |    const std::unordered_set<types::TextEventKey, | ||||||
|  |                             types::TextEventHash<types::TextEventKey>>& | ||||||
|  |       alertKeys) | ||||||
|  | { | ||||||
|  |    logger_->trace("Handle alerts removed"); | ||||||
|  | 
 | ||||||
|  |    for (const auto& alertKey : alertKeys) | ||||||
|  |    { | ||||||
|  |       // Remove from the list of text event keys
 | ||||||
|  |       auto it = std::find( | ||||||
|  |          p->textEventKeys_.begin(), p->textEventKeys_.end(), alertKey); | ||||||
|  |       if (it != p->textEventKeys_.end()) | ||||||
|  |       { | ||||||
|  |          const int row = | ||||||
|  |             static_cast<int>(std::distance(p->textEventKeys_.begin(), it)); | ||||||
|  |          beginRemoveRows(QModelIndex(), row, row); | ||||||
|  |          p->textEventKeys_.erase(it); | ||||||
|  |          endRemoveRows(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // Remove from internal maps
 | ||||||
|  |       p->observedMap_.erase(alertKey); | ||||||
|  |       p->threatCategoryMap_.erase(alertKey); | ||||||
|  |       p->tornadoPossibleMap_.erase(alertKey); | ||||||
|  |       p->centroidMap_.erase(alertKey); | ||||||
|  |       p->distanceMap_.erase(alertKey); | ||||||
|  |    } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| void AlertModel::HandleMapUpdate(double latitude, double longitude) | void AlertModel::HandleMapUpdate(double latitude, double longitude) | ||||||
| { | { | ||||||
|    logger_->trace("Handle map update: {}, {}", latitude, longitude); |    logger_->trace("Handle map update: {}, {}", latitude, longitude); | ||||||
|  | @ -488,7 +541,7 @@ std::string AlertModelImpl::GetCounties(const types::TextEventKey& key) | ||||||
|    } |    } | ||||||
|    else |    else | ||||||
|    { |    { | ||||||
|       logger_->warn("GetCounties(): No message associated with key: {}", |       logger_->trace("GetCounties(): No message associated with key: {}", | ||||||
|                      key.ToString()); |                      key.ToString()); | ||||||
|       return {}; |       return {}; | ||||||
|    } |    } | ||||||
|  | @ -507,7 +560,7 @@ std::string AlertModelImpl::GetState(const types::TextEventKey& key) | ||||||
|    } |    } | ||||||
|    else |    else | ||||||
|    { |    { | ||||||
|       logger_->warn("GetState(): No message associated with key: {}", |       logger_->trace("GetState(): No message associated with key: {}", | ||||||
|                      key.ToString()); |                      key.ToString()); | ||||||
|       return {}; |       return {}; | ||||||
|    } |    } | ||||||
|  | @ -525,7 +578,7 @@ AlertModelImpl::GetStartTime(const types::TextEventKey& key) | ||||||
|    } |    } | ||||||
|    else |    else | ||||||
|    { |    { | ||||||
|       logger_->warn("GetStartTime(): No message associated with key: {}", |       logger_->trace("GetStartTime(): No message associated with key: {}", | ||||||
|                      key.ToString()); |                      key.ToString()); | ||||||
|       return {}; |       return {}; | ||||||
|    } |    } | ||||||
|  | @ -550,7 +603,7 @@ AlertModelImpl::GetEndTime(const types::TextEventKey& key) | ||||||
|    } |    } | ||||||
|    else |    else | ||||||
|    { |    { | ||||||
|       logger_->warn("GetEndTime(): No message associated with key: {}", |       logger_->trace("GetEndTime(): No message associated with key: {}", | ||||||
|                      key.ToString()); |                      key.ToString()); | ||||||
|       return {}; |       return {}; | ||||||
|    } |    } | ||||||
|  | @ -561,6 +614,4 @@ std::string AlertModelImpl::GetEndTimeString(const types::TextEventKey& key) | ||||||
|    return scwx::util::TimeString(GetEndTime(key)); |    return scwx::util::TimeString(GetEndTime(key)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| } // namespace model
 | } // namespace scwx::qt::model
 | ||||||
| } // namespace qt
 |  | ||||||
| } // namespace scwx
 |  | ||||||
|  |  | ||||||
|  | @ -4,7 +4,9 @@ | ||||||
| #include <scwx/common/geographic.hpp> | #include <scwx/common/geographic.hpp> | ||||||
| 
 | 
 | ||||||
| #include <memory> | #include <memory> | ||||||
|  | #include <unordered_set> | ||||||
| 
 | 
 | ||||||
|  | #include <boost/uuid/uuid.hpp> | ||||||
| #include <QAbstractTableModel> | #include <QAbstractTableModel> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx | ||||||
|  | @ -50,7 +52,13 @@ public: | ||||||
|                        int             role = Qt::DisplayRole) const override; |                        int             role = Qt::DisplayRole) const override; | ||||||
| 
 | 
 | ||||||
| public slots: | public slots: | ||||||
|    void HandleAlert(const types::TextEventKey& alertKey, size_t messageIndex); |    void HandleAlert(const types::TextEventKey& alertKey, | ||||||
|  |                     std::size_t                messageIndex, | ||||||
|  |                     boost::uuids::uuid         uuid); | ||||||
|  |    void HandleAlertsRemoved( | ||||||
|  |       const std::unordered_set<types::TextEventKey, | ||||||
|  |                                types::TextEventHash<types::TextEventKey>>& | ||||||
|  |          alertKeys); | ||||||
|    void HandleMapUpdate(double latitude, double longitude); |    void HandleMapUpdate(double latitude, double longitude); | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|  |  | ||||||
|  | @ -9,21 +9,22 @@ | ||||||
| 
 | 
 | ||||||
| #include <boost/asio/steady_timer.hpp> | #include <boost/asio/steady_timer.hpp> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx::qt::model | ||||||
| { |  | ||||||
| namespace qt |  | ||||||
| { |  | ||||||
| namespace model |  | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| static const std::string logPrefix_ = "scwx::qt::model::alert_proxy_model"; | static const std::string logPrefix_ = "scwx::qt::model::alert_proxy_model"; | ||||||
| static const auto        logger_    = scwx::util::Logger::Create(logPrefix_); | static const auto        logger_    = scwx::util::Logger::Create(logPrefix_); | ||||||
| 
 | 
 | ||||||
| class AlertProxyModelImpl | class AlertProxyModel::Impl | ||||||
| { | { | ||||||
| public: | public: | ||||||
|    explicit AlertProxyModelImpl(AlertProxyModel* self); |    explicit Impl(AlertProxyModel* self); | ||||||
|    ~AlertProxyModelImpl(); |    ~Impl(); | ||||||
|  | 
 | ||||||
|  |    Impl(const Impl&)             = delete; | ||||||
|  |    Impl& operator=(const Impl&)  = delete; | ||||||
|  |    Impl(const Impl&&)            = delete; | ||||||
|  |    Impl& operator=(const Impl&&) = delete; | ||||||
| 
 | 
 | ||||||
|    void UpdateAlerts(); |    void UpdateAlerts(); | ||||||
| 
 | 
 | ||||||
|  | @ -36,8 +37,7 @@ public: | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| AlertProxyModel::AlertProxyModel(QObject* parent) : | AlertProxyModel::AlertProxyModel(QObject* parent) : | ||||||
|     QSortFilterProxyModel(parent), |     QSortFilterProxyModel(parent), p(std::make_unique<Impl>(this)) | ||||||
|     p(std::make_unique<AlertProxyModelImpl>(this)) |  | ||||||
| { | { | ||||||
| } | } | ||||||
| AlertProxyModel::~AlertProxyModel() = default; | AlertProxyModel::~AlertProxyModel() = default; | ||||||
|  | @ -77,7 +77,7 @@ bool AlertProxyModel::filterAcceptsRow(int                sourceRow, | ||||||
|           QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); |           QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| AlertProxyModelImpl::AlertProxyModelImpl(AlertProxyModel* self) : | AlertProxyModel::Impl::Impl(AlertProxyModel* self) : | ||||||
|     self_ {self}, |     self_ {self}, | ||||||
|     alertActiveFilterEnabled_ {false}, |     alertActiveFilterEnabled_ {false}, | ||||||
|     alertUpdateTimer_ {scwx::util::io_context()} |     alertUpdateTimer_ {scwx::util::io_context()} | ||||||
|  | @ -86,26 +86,37 @@ AlertProxyModelImpl::AlertProxyModelImpl(AlertProxyModel* self) : | ||||||
|    UpdateAlerts(); |    UpdateAlerts(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| AlertProxyModelImpl::~AlertProxyModelImpl() | AlertProxyModel::Impl::~Impl() | ||||||
| { | { | ||||||
|    std::unique_lock lock(alertMutex_); |    try | ||||||
|  |    { | ||||||
|  |       const std::unique_lock lock(alertMutex_); | ||||||
|       alertUpdateTimer_.cancel(); |       alertUpdateTimer_.cancel(); | ||||||
|    } |    } | ||||||
|  |    catch (const std::exception& ex) | ||||||
|  |    { | ||||||
|  |       logger_->error(ex.what()); | ||||||
|  |    } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| void AlertProxyModelImpl::UpdateAlerts() | void AlertProxyModel::Impl::UpdateAlerts() | ||||||
| { | { | ||||||
|    logger_->trace("UpdateAlerts"); |    logger_->trace("UpdateAlerts"); | ||||||
| 
 | 
 | ||||||
|    // Take a unique lock before modifying feature lists
 |    // Take a unique lock before modifying feature lists
 | ||||||
|    std::unique_lock lock(alertMutex_); |    const std::unique_lock lock(alertMutex_); | ||||||
| 
 | 
 | ||||||
|    // Re-evaluate for expired alerts
 |    // Re-evaluate for expired alerts
 | ||||||
|    if (alertActiveFilterEnabled_) |    if (alertActiveFilterEnabled_) | ||||||
|    { |    { | ||||||
|       self_->invalidateRowsFilter(); |       QMetaObject::invokeMethod(self_, | ||||||
|  |                                 static_cast<void (QSortFilterProxyModel::*)()>( | ||||||
|  |                                    &QSortFilterProxyModel::invalidate)); | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|    using namespace std::chrono; |    using namespace std::chrono; | ||||||
|  | 
 | ||||||
|  |    // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers): Readability
 | ||||||
|    alertUpdateTimer_.expires_after(15s); |    alertUpdateTimer_.expires_after(15s); | ||||||
|    alertUpdateTimer_.async_wait( |    alertUpdateTimer_.async_wait( | ||||||
|       [this](const boost::system::error_code& e) |       [this](const boost::system::error_code& e) | ||||||
|  | @ -132,6 +143,4 @@ void AlertProxyModelImpl::UpdateAlerts() | ||||||
|       }); |       }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| } // namespace model
 | } // namespace scwx::qt::model
 | ||||||
| } // namespace qt
 |  | ||||||
| } // namespace scwx
 |  | ||||||
|  |  | ||||||
|  | @ -4,11 +4,7 @@ | ||||||
| 
 | 
 | ||||||
| #include <QSortFilterProxyModel> | #include <QSortFilterProxyModel> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx::qt::model | ||||||
| { |  | ||||||
| namespace qt |  | ||||||
| { |  | ||||||
| namespace model |  | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| class AlertProxyModelImpl; | class AlertProxyModelImpl; | ||||||
|  | @ -16,7 +12,7 @@ class AlertProxyModelImpl; | ||||||
| class AlertProxyModel : public QSortFilterProxyModel | class AlertProxyModel : public QSortFilterProxyModel | ||||||
| { | { | ||||||
| private: | private: | ||||||
|    Q_DISABLE_COPY(AlertProxyModel) |    Q_DISABLE_COPY_MOVE(AlertProxyModel) | ||||||
| 
 | 
 | ||||||
| public: | public: | ||||||
|    explicit AlertProxyModel(QObject* parent = nullptr); |    explicit AlertProxyModel(QObject* parent = nullptr); | ||||||
|  | @ -24,15 +20,13 @@ public: | ||||||
| 
 | 
 | ||||||
|    void SetAlertActiveFilter(bool enabled); |    void SetAlertActiveFilter(bool enabled); | ||||||
| 
 | 
 | ||||||
|    bool filterAcceptsRow(int                sourceRow, |    [[nodiscard]] bool | ||||||
|  |    filterAcceptsRow(int                sourceRow, | ||||||
|                     const QModelIndex& sourceParent) const override; |                     const QModelIndex& sourceParent) const override; | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|    std::unique_ptr<AlertProxyModelImpl> p; |    class Impl; | ||||||
| 
 |    std::unique_ptr<Impl> p; | ||||||
|    friend class AlertProxyModelImpl; |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| } // namespace model
 | } // namespace scwx::qt::model
 | ||||||
| } // namespace qt
 |  | ||||||
| } // namespace scwx
 |  | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| #include <scwx/qt/model/layer_model.hpp> | #include <scwx/qt/model/layer_model.hpp> | ||||||
| #include <scwx/qt/manager/placefile_manager.hpp> | #include <scwx/qt/manager/placefile_manager.hpp> | ||||||
| #include <scwx/qt/types/qt_types.hpp> | #include <scwx/qt/types/qt_types.hpp> | ||||||
| #include <scwx/qt/util/json.hpp> | #include <scwx/util/json.hpp> | ||||||
| #include <scwx/util/logger.hpp> | #include <scwx/util/logger.hpp> | ||||||
| 
 | 
 | ||||||
| #include <filesystem> | #include <filesystem> | ||||||
|  |  | ||||||
|  | @ -4,8 +4,8 @@ | ||||||
| #include <scwx/qt/types/qt_types.hpp> | #include <scwx/qt/types/qt_types.hpp> | ||||||
| #include <scwx/qt/types/unit_types.hpp> | #include <scwx/qt/types/unit_types.hpp> | ||||||
| #include <scwx/qt/util/geographic_lib.hpp> | #include <scwx/qt/util/geographic_lib.hpp> | ||||||
| #include <scwx/qt/util/json.hpp> |  | ||||||
| #include <scwx/common/geographic.hpp> | #include <scwx/common/geographic.hpp> | ||||||
|  | #include <scwx/util/json.hpp> | ||||||
| #include <scwx/util/logger.hpp> | #include <scwx/util/logger.hpp> | ||||||
| 
 | 
 | ||||||
| #include <filesystem> | #include <filesystem> | ||||||
|  | @ -117,7 +117,7 @@ void RadarSiteModelImpl::ReadPresets() | ||||||
|    // Determine if presets exists
 |    // Determine if presets exists
 | ||||||
|    if (std::filesystem::exists(presetsPath_)) |    if (std::filesystem::exists(presetsPath_)) | ||||||
|    { |    { | ||||||
|       presetsJson = util::json::ReadJsonFile(presetsPath_); |       presetsJson = scwx::util::json::ReadJsonFile(presetsPath_); | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|    // If presets was successfully read
 |    // If presets was successfully read
 | ||||||
|  | @ -160,7 +160,7 @@ void RadarSiteModelImpl::WritePresets() | ||||||
|    logger_->info("Saving presets"); |    logger_->info("Saving presets"); | ||||||
| 
 | 
 | ||||||
|    auto presetsJson = boost::json::value_from(presets_); |    auto presetsJson = boost::json::value_from(presets_); | ||||||
|    util::json::WriteJsonFile(presetsPath_, presetsJson); |    scwx::util::json::WriteJsonFile(presetsPath_, presetsJson); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| int RadarSiteModel::rowCount(const QModelIndex& parent) const | int RadarSiteModel::rowCount(const QModelIndex& parent) const | ||||||
|  |  | ||||||
|  | @ -14,26 +14,29 @@ static const std::string logPrefix_ = "scwx::qt::types::text_event_key"; | ||||||
| 
 | 
 | ||||||
| std::string TextEventKey::ToFullString() const | std::string TextEventKey::ToFullString() const | ||||||
| { | { | ||||||
|    return fmt::format("{} {} {} {:04}", |    return fmt::format("{} {} {} {:04} ({:04})", | ||||||
|                       officeId_, |                       officeId_, | ||||||
|                       awips::GetPhenomenonText(phenomenon_), |                       awips::GetPhenomenonText(phenomenon_), | ||||||
|                       awips::GetSignificanceText(significance_), |                       awips::GetSignificanceText(significance_), | ||||||
|                       etn_); |                       etn_, | ||||||
|  |                       static_cast<int>(year_)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| std::string TextEventKey::ToString() const | std::string TextEventKey::ToString() const | ||||||
| { | { | ||||||
|    return fmt::format("{}.{}.{}.{:04}", |    return fmt::format("{}.{}.{}.{:04}.{:04}", | ||||||
|                       officeId_, |                       officeId_, | ||||||
|                       awips::GetPhenomenonCode(phenomenon_), |                       awips::GetPhenomenonCode(phenomenon_), | ||||||
|                       awips::GetSignificanceCode(significance_), |                       awips::GetSignificanceCode(significance_), | ||||||
|                       etn_); |                       etn_, | ||||||
|  |                       static_cast<int>(year_)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| bool TextEventKey::operator==(const TextEventKey& o) const | bool TextEventKey::operator==(const TextEventKey& o) const | ||||||
| { | { | ||||||
|    return (officeId_ == o.officeId_ && phenomenon_ == o.phenomenon_ && |    return (officeId_ == o.officeId_ && phenomenon_ == o.phenomenon_ && | ||||||
|            significance_ == o.significance_ && etn_ == o.etn_); |            significance_ == o.significance_ && etn_ == o.etn_ && | ||||||
|  |            year_ == o.year_); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| size_t TextEventHash<TextEventKey>::operator()(const TextEventKey& x) const | size_t TextEventHash<TextEventKey>::operator()(const TextEventKey& x) const | ||||||
|  | @ -43,6 +46,7 @@ size_t TextEventHash<TextEventKey>::operator()(const TextEventKey& x) const | ||||||
|    boost::hash_combine(seed, x.phenomenon_); |    boost::hash_combine(seed, x.phenomenon_); | ||||||
|    boost::hash_combine(seed, x.significance_); |    boost::hash_combine(seed, x.significance_); | ||||||
|    boost::hash_combine(seed, x.etn_); |    boost::hash_combine(seed, x.etn_); | ||||||
|  |    boost::hash_combine(seed, static_cast<int>(x.year_)); | ||||||
|    return seed; |    return seed; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| #pragma once | #pragma once | ||||||
| 
 | 
 | ||||||
| #include <scwx/awips/pvtec.hpp> | #include <scwx/awips/pvtec.hpp> | ||||||
|  | #include <scwx/awips/wmo_header.hpp> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx | ||||||
| { | { | ||||||
|  | @ -12,12 +13,34 @@ namespace types | ||||||
| struct TextEventKey | struct TextEventKey | ||||||
| { | { | ||||||
|    TextEventKey() : TextEventKey(awips::PVtec {}) {} |    TextEventKey() : TextEventKey(awips::PVtec {}) {} | ||||||
|    TextEventKey(const awips::PVtec& pvtec) : |    TextEventKey(const awips::PVtec& pvtec, std::chrono::year yearHint = {}) : | ||||||
|        officeId_ {pvtec.office_id()}, |        officeId_ {pvtec.office_id()}, | ||||||
|        phenomenon_ {pvtec.phenomenon()}, |        phenomenon_ {pvtec.phenomenon()}, | ||||||
|        significance_ {pvtec.significance()}, |        significance_ {pvtec.significance()}, | ||||||
|        etn_ {pvtec.event_tracking_number()} |        etn_ {pvtec.event_tracking_number()} | ||||||
|    { |    { | ||||||
|  |       using namespace std::chrono_literals; | ||||||
|  | 
 | ||||||
|  |       static constexpr std::chrono::year kMinYear_ = 1970y; | ||||||
|  | 
 | ||||||
|  |       std::chrono::year_month_day ymd = | ||||||
|  |          std::chrono::floor<std::chrono::days>(pvtec.event_begin()); | ||||||
|  |       if (ymd.year() > kMinYear_) | ||||||
|  |       { | ||||||
|  |          // Prefer the year from the event begin
 | ||||||
|  |          year_ = ymd.year(); | ||||||
|  |       } | ||||||
|  |       else if (yearHint > kMinYear_) | ||||||
|  |       { | ||||||
|  |          // Otherwise, use the year hint
 | ||||||
|  |          year_ = yearHint; | ||||||
|  |       } | ||||||
|  |       else | ||||||
|  |       { | ||||||
|  |          // If there was no year hint, use the event end
 | ||||||
|  |          ymd   = std::chrono::floor<std::chrono::days>(pvtec.event_end()); | ||||||
|  |          year_ = ymd.year(); | ||||||
|  |       } | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|    std::string ToFullString() const; |    std::string ToFullString() const; | ||||||
|  | @ -27,7 +50,8 @@ struct TextEventKey | ||||||
|    std::string         officeId_; |    std::string         officeId_; | ||||||
|    awips::Phenomenon   phenomenon_; |    awips::Phenomenon   phenomenon_; | ||||||
|    awips::Significance significance_; |    awips::Significance significance_; | ||||||
|    int16_t             etn_; |    std::int16_t        etn_; | ||||||
|  |    std::chrono::year   year_ {}; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| template<class Key> | template<class Key> | ||||||
|  |  | ||||||
|  | @ -131,6 +131,11 @@ void AlertDockWidgetImpl::ConnectSignals() | ||||||
|            &QAction::toggled, |            &QAction::toggled, | ||||||
|            proxyModel_.get(), |            proxyModel_.get(), | ||||||
|            &model::AlertProxyModel::SetAlertActiveFilter); |            &model::AlertProxyModel::SetAlertActiveFilter); | ||||||
|  |    connect(textEventManager_.get(), | ||||||
|  |            &manager::TextEventManager::AlertsRemoved, | ||||||
|  |            alertModel_.get(), | ||||||
|  |            &model::AlertModel::HandleAlertsRemoved, | ||||||
|  |            Qt::QueuedConnection); | ||||||
|    connect(textEventManager_.get(), |    connect(textEventManager_.get(), | ||||||
|            &manager::TextEventManager::AlertUpdated, |            &manager::TextEventManager::AlertUpdated, | ||||||
|            alertModel_.get(), |            alertModel_.get(), | ||||||
|  |  | ||||||
|  | @ -1,41 +1,19 @@ | ||||||
| #include <scwx/qt/util/json.hpp> | #include <scwx/qt/util/json.hpp> | ||||||
|  | #include <scwx/util/json.hpp> | ||||||
| #include <scwx/util/logger.hpp> | #include <scwx/util/logger.hpp> | ||||||
| 
 | 
 | ||||||
| #include <fstream> |  | ||||||
| 
 |  | ||||||
| #include <boost/json.hpp> |  | ||||||
| #include <fmt/ranges.h> |  | ||||||
| #include <QFile> | #include <QFile> | ||||||
| #include <QTextStream> | #include <QTextStream> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx::qt::util::json | ||||||
| { |  | ||||||
| namespace qt |  | ||||||
| { |  | ||||||
| namespace util |  | ||||||
| { |  | ||||||
| namespace json |  | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| static const std::string logPrefix_ = "scwx::qt::util::json"; | static const std::string logPrefix_ = "scwx::qt::util::json"; | ||||||
| static const auto        logger_    = scwx::util::Logger::Create(logPrefix_); | static const auto        logger_    = scwx::util::Logger::Create(logPrefix_); | ||||||
| 
 | 
 | ||||||
| /* Adapted from:
 |  | ||||||
|  * https://www.boost.org/doc/libs/1_77_0/libs/json/doc/html/json/examples.html#json.examples.pretty
 |  | ||||||
|  * |  | ||||||
|  * Copyright (c) 2019, 2020 Vinnie Falco |  | ||||||
|  * Copyright (c) 2020 Krystian Stasiowski |  | ||||||
|  * Distributed under the Boost Software License, Version 1.0. (See |  | ||||||
|  * http://www.boost.org/LICENSE_1_0.txt)
 |  | ||||||
|  */ |  | ||||||
| static void PrettyPrintJson(std::ostream&             os, |  | ||||||
|                             boost::json::value const& jv, |  | ||||||
|                             std::string*              indent = nullptr); |  | ||||||
| 
 |  | ||||||
| static boost::json::value ReadJsonFile(QFile& file); | static boost::json::value ReadJsonFile(QFile& file); | ||||||
| static boost::json::value ReadJsonStream(std::istream& is); |  | ||||||
| 
 | 
 | ||||||
| boost::json::value ReadJsonFile(const std::string& path) | boost::json::value ReadJsonQFile(const std::string& path) | ||||||
| { | { | ||||||
|    boost::json::value json; |    boost::json::value json; | ||||||
| 
 | 
 | ||||||
|  | @ -46,8 +24,7 @@ boost::json::value ReadJsonFile(const std::string& path) | ||||||
|    } |    } | ||||||
|    else |    else | ||||||
|    { |    { | ||||||
|       std::ifstream ifs {path}; |       json = ::scwx::util::json::ReadJsonFile(path); | ||||||
|       json = ReadJsonStream(ifs); |  | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|    return json; |    return json; | ||||||
|  | @ -65,7 +42,7 @@ static boost::json::value ReadJsonFile(QFile& file) | ||||||
|       std::string        jsonSource = jsonStream.readAll().toStdString(); |       std::string        jsonSource = jsonStream.readAll().toStdString(); | ||||||
|       std::istringstream is {jsonSource}; |       std::istringstream is {jsonSource}; | ||||||
| 
 | 
 | ||||||
|       json = ReadJsonStream(is); |       json = ::scwx::util::json::ReadJsonStream(is); | ||||||
| 
 | 
 | ||||||
|       file.close(); |       file.close(); | ||||||
|    } |    } | ||||||
|  | @ -78,147 +55,4 @@ static boost::json::value ReadJsonFile(QFile& file) | ||||||
|    return json; |    return json; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static boost::json::value ReadJsonStream(std::istream& is) | } // namespace scwx::qt::util::json
 | ||||||
| { |  | ||||||
|    std::string line; |  | ||||||
| 
 |  | ||||||
|    boost::json::stream_parser p; |  | ||||||
|    boost::system::error_code  ec; |  | ||||||
| 
 |  | ||||||
|    while (std::getline(is, line)) |  | ||||||
|    { |  | ||||||
|       p.write(line, ec); |  | ||||||
|       if (ec) |  | ||||||
|       { |  | ||||||
|          logger_->warn("{}", ec.message()); |  | ||||||
|          return nullptr; |  | ||||||
|       } |  | ||||||
|    } |  | ||||||
| 
 |  | ||||||
|    p.finish(ec); |  | ||||||
|    if (ec) |  | ||||||
|    { |  | ||||||
|       logger_->warn("{}", ec.message()); |  | ||||||
|       return nullptr; |  | ||||||
|    } |  | ||||||
| 
 |  | ||||||
|    return p.release(); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void WriteJsonFile(const std::string&        path, |  | ||||||
|                    const boost::json::value& json, |  | ||||||
|                    bool                      prettyPrint) |  | ||||||
| { |  | ||||||
|    std::ofstream ofs {path}; |  | ||||||
| 
 |  | ||||||
|    if (!ofs.is_open()) |  | ||||||
|    { |  | ||||||
|       logger_->warn("Cannot write JSON file: \"{}\"", path); |  | ||||||
|    } |  | ||||||
|    else |  | ||||||
|    { |  | ||||||
|       if (prettyPrint) |  | ||||||
|       { |  | ||||||
|          PrettyPrintJson(ofs, json); |  | ||||||
|       } |  | ||||||
|       else |  | ||||||
|       { |  | ||||||
|          ofs << json; |  | ||||||
|       } |  | ||||||
|       ofs.close(); |  | ||||||
|    } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| static void PrettyPrintJson(std::ostream&             os, |  | ||||||
|                             boost::json::value const& jv, |  | ||||||
|                             std::string*              indent) |  | ||||||
| { |  | ||||||
|    std::string indent_; |  | ||||||
|    if (!indent) |  | ||||||
|       indent = &indent_; |  | ||||||
|    switch (jv.kind()) |  | ||||||
|    { |  | ||||||
|    case boost::json::kind::object: |  | ||||||
|    { |  | ||||||
|       os << "{\n"; |  | ||||||
|       indent->append(4, ' '); |  | ||||||
|       auto const& obj = jv.get_object(); |  | ||||||
|       if (!obj.empty()) |  | ||||||
|       { |  | ||||||
|          auto it = obj.begin(); |  | ||||||
|          for (;;) |  | ||||||
|          { |  | ||||||
|             os << *indent << boost::json::serialize(it->key()) << " : "; |  | ||||||
|             PrettyPrintJson(os, it->value(), indent); |  | ||||||
|             if (++it == obj.end()) |  | ||||||
|                break; |  | ||||||
|             os << ",\n"; |  | ||||||
|          } |  | ||||||
|       } |  | ||||||
|       os << "\n"; |  | ||||||
|       indent->resize(indent->size() - 4); |  | ||||||
|       os << *indent << "}"; |  | ||||||
|       break; |  | ||||||
|    } |  | ||||||
| 
 |  | ||||||
|    case boost::json::kind::array: |  | ||||||
|    { |  | ||||||
|       os << "[\n"; |  | ||||||
|       indent->append(4, ' '); |  | ||||||
|       auto const& arr = jv.get_array(); |  | ||||||
|       if (!arr.empty()) |  | ||||||
|       { |  | ||||||
|          auto it = arr.begin(); |  | ||||||
|          for (;;) |  | ||||||
|          { |  | ||||||
|             os << *indent; |  | ||||||
|             PrettyPrintJson(os, *it, indent); |  | ||||||
|             if (++it == arr.end()) |  | ||||||
|                break; |  | ||||||
|             os << ",\n"; |  | ||||||
|          } |  | ||||||
|       } |  | ||||||
|       os << "\n"; |  | ||||||
|       indent->resize(indent->size() - 4); |  | ||||||
|       os << *indent << "]"; |  | ||||||
|       break; |  | ||||||
|    } |  | ||||||
| 
 |  | ||||||
|    case boost::json::kind::string: |  | ||||||
|    { |  | ||||||
|       os << boost::json::serialize(jv.get_string()); |  | ||||||
|       break; |  | ||||||
|    } |  | ||||||
| 
 |  | ||||||
|    case boost::json::kind::uint64: |  | ||||||
|       os << jv.get_uint64(); |  | ||||||
|       break; |  | ||||||
| 
 |  | ||||||
|    case boost::json::kind::int64: |  | ||||||
|       os << jv.get_int64(); |  | ||||||
|       break; |  | ||||||
| 
 |  | ||||||
|    case boost::json::kind::double_: |  | ||||||
|       os << jv.get_double(); |  | ||||||
|       break; |  | ||||||
| 
 |  | ||||||
|    case boost::json::kind::bool_: |  | ||||||
|       if (jv.get_bool()) |  | ||||||
|          os << "true"; |  | ||||||
|       else |  | ||||||
|          os << "false"; |  | ||||||
|       break; |  | ||||||
| 
 |  | ||||||
|    case boost::json::kind::null: |  | ||||||
|       os << "null"; |  | ||||||
|       break; |  | ||||||
|    } |  | ||||||
| 
 |  | ||||||
|    if (indent->empty()) |  | ||||||
|       os << "\n"; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| } // namespace json
 |  | ||||||
| } // namespace util
 |  | ||||||
| } // namespace qt
 |  | ||||||
| } // namespace scwx
 |  | ||||||
|  |  | ||||||
|  | @ -1,7 +1,5 @@ | ||||||
| #pragma once | #pragma once | ||||||
| 
 | 
 | ||||||
| #include <optional> |  | ||||||
| 
 |  | ||||||
| #include <boost/json/value.hpp> | #include <boost/json/value.hpp> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx | ||||||
|  | @ -13,10 +11,7 @@ namespace util | ||||||
| namespace json | namespace json | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| boost::json::value ReadJsonFile(const std::string& path); | boost::json::value ReadJsonQFile(const std::string& path); | ||||||
| void               WriteJsonFile(const std::string&        path, |  | ||||||
|                                  const boost::json::value& json, |  | ||||||
|                                  bool                      prettyPrint = true); |  | ||||||
| 
 | 
 | ||||||
| } // namespace json
 | } // namespace json
 | ||||||
| } // namespace util
 | } // namespace util
 | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								test/.clang-tidy
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								test/.clang-tidy
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | Checks: | ||||||
|  |   - '-*' | ||||||
|  |   - 'bugprone-*' | ||||||
|  |   - 'clang-analyzer-*' | ||||||
|  |   - 'cppcoreguidelines-*' | ||||||
|  |   - 'misc-*' | ||||||
|  |   - 'modernize-*' | ||||||
|  |   - 'performance-*' | ||||||
|  |   - '-bugprone-easily-swappable-parameters' | ||||||
|  |   - '-cppcoreguidelines-avoid-magic-numbers' | ||||||
|  |   - '-cppcoreguidelines-pro-type-reinterpret-cast' | ||||||
|  |   - '-misc-include-cleaner' | ||||||
|  |   - '-misc-non-private-member-variables-in-classes' | ||||||
|  |   - '-modernize-return-braced-init-list' | ||||||
|  |   - '-modernize-use-trailing-return-type' | ||||||
|  | FormatStyle: 'file' | ||||||
							
								
								
									
										130
									
								
								test/source/scwx/awips/wmo_header.test.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								test/source/scwx/awips/wmo_header.test.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,130 @@ | ||||||
|  | #include <scwx/awips/wmo_header.hpp> | ||||||
|  | 
 | ||||||
|  | #include <gtest/gtest.h> | ||||||
|  | 
 | ||||||
|  | namespace scwx::awips | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | static const std::string logPrefix_ = "scwx::awips::wmo_header.test"; | ||||||
|  | 
 | ||||||
|  | static const std::string kWmoHeaderSample_ { | ||||||
|  |    "887\n" | ||||||
|  |    "WFUS54 KOUN 280044\n" | ||||||
|  |    "TOROUN"}; | ||||||
|  | 
 | ||||||
|  | TEST(WmoHeader, WmoFields) | ||||||
|  | { | ||||||
|  |    std::stringstream ss {kWmoHeaderSample_}; | ||||||
|  |    WmoHeader         header; | ||||||
|  |    const bool        valid = header.Parse(ss); | ||||||
|  | 
 | ||||||
|  |    EXPECT_EQ(valid, true); | ||||||
|  |    EXPECT_EQ(header.sequence_number(), "887"); | ||||||
|  |    EXPECT_EQ(header.data_type(), "WF"); | ||||||
|  |    EXPECT_EQ(header.geographic_designator(), "US"); | ||||||
|  |    EXPECT_EQ(header.bulletin_id(), "54"); | ||||||
|  |    EXPECT_EQ(header.icao(), "KOUN"); | ||||||
|  |    EXPECT_EQ(header.date_time(), "280044"); | ||||||
|  |    EXPECT_EQ(header.bbb_indicator(), ""); | ||||||
|  |    EXPECT_EQ(header.product_category(), "TOR"); | ||||||
|  |    EXPECT_EQ(header.product_designator(), "OUN"); | ||||||
|  |    EXPECT_EQ(header.GetDateTime(), | ||||||
|  |              std::chrono::sys_time<std::chrono::minutes> {}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | TEST(WmoHeader, DateHintBeforeParse) | ||||||
|  | { | ||||||
|  |    using namespace std::chrono; | ||||||
|  | 
 | ||||||
|  |    std::stringstream ss {kWmoHeaderSample_}; | ||||||
|  |    WmoHeader         header; | ||||||
|  | 
 | ||||||
|  |    header.SetDateHint(2022y / October); | ||||||
|  |    const bool valid = header.Parse(ss); | ||||||
|  | 
 | ||||||
|  |    EXPECT_EQ(valid, true); | ||||||
|  |    EXPECT_EQ(header.GetDateTime(), | ||||||
|  |              sys_days {2022y / October / 28d} + 0h + 44min); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | TEST(WmoHeader, DateHintAfterParse) | ||||||
|  | { | ||||||
|  |    using namespace std::chrono; | ||||||
|  | 
 | ||||||
|  |    std::stringstream ss {kWmoHeaderSample_}; | ||||||
|  |    WmoHeader         header; | ||||||
|  | 
 | ||||||
|  |    const bool valid = header.Parse(ss); | ||||||
|  |    header.SetDateHint(2022y / October); | ||||||
|  | 
 | ||||||
|  |    EXPECT_EQ(valid, true); | ||||||
|  |    EXPECT_EQ(header.GetDateTime(), | ||||||
|  |              sys_days {2022y / October / 28d} + 0h + 44min); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | TEST(WmoHeader, EndTimeHintSameMonth) | ||||||
|  | { | ||||||
|  |    using namespace std::chrono; | ||||||
|  | 
 | ||||||
|  |    std::stringstream ss {kWmoHeaderSample_}; | ||||||
|  |    WmoHeader         header; | ||||||
|  | 
 | ||||||
|  |    const bool valid = header.Parse(ss); | ||||||
|  | 
 | ||||||
|  |    auto endTimeHint = sys_days {2022y / October / 29d} + 0h + 0min + 0s; | ||||||
|  | 
 | ||||||
|  |    EXPECT_EQ(valid, true); | ||||||
|  |    EXPECT_EQ(header.GetDateTime(endTimeHint), | ||||||
|  |              sys_days {2022y / October / 28d} + 0h + 44min); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | TEST(WmoHeader, EndTimeHintPreviousMonth) | ||||||
|  | { | ||||||
|  |    using namespace std::chrono; | ||||||
|  | 
 | ||||||
|  |    std::stringstream ss {kWmoHeaderSample_}; | ||||||
|  |    WmoHeader         header; | ||||||
|  | 
 | ||||||
|  |    const bool valid = header.Parse(ss); | ||||||
|  | 
 | ||||||
|  |    auto endTimeHint = sys_days {2022y / October / 27d} + 0h + 0min + 0s; | ||||||
|  | 
 | ||||||
|  |    EXPECT_EQ(valid, true); | ||||||
|  |    EXPECT_EQ(header.GetDateTime(endTimeHint), | ||||||
|  |              sys_days {2022y / September / 28d} + 0h + 44min); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | TEST(WmoHeader, EndTimeHintPreviousYear) | ||||||
|  | { | ||||||
|  |    using namespace std::chrono; | ||||||
|  | 
 | ||||||
|  |    std::stringstream ss {kWmoHeaderSample_}; | ||||||
|  |    WmoHeader         header; | ||||||
|  | 
 | ||||||
|  |    const bool valid = header.Parse(ss); | ||||||
|  | 
 | ||||||
|  |    auto endTimeHint = sys_days {2022y / January / 27d} + 0h + 0min + 0s; | ||||||
|  | 
 | ||||||
|  |    EXPECT_EQ(valid, true); | ||||||
|  |    EXPECT_EQ(header.GetDateTime(endTimeHint), | ||||||
|  |              sys_days {2021y / December / 28d} + 0h + 44min); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | TEST(WmoHeader, EndTimeHintIgnored) | ||||||
|  | { | ||||||
|  |    using namespace std::chrono; | ||||||
|  | 
 | ||||||
|  |    std::stringstream ss {kWmoHeaderSample_}; | ||||||
|  |    WmoHeader         header; | ||||||
|  | 
 | ||||||
|  |    header.SetDateHint(2022y / October); | ||||||
|  |    const bool valid = header.Parse(ss); | ||||||
|  | 
 | ||||||
|  |    auto endTimeHint = sys_days {2020y / January / 1d} + 0h + 0min + 0s; | ||||||
|  | 
 | ||||||
|  |    EXPECT_EQ(valid, true); | ||||||
|  |    EXPECT_EQ(header.GetDateTime(endTimeHint), | ||||||
|  |              sys_days {2022y / October / 28d} + 0h + 44min); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } // namespace scwx::awips
 | ||||||
							
								
								
									
										57
									
								
								test/source/scwx/provider/iem_api_provider.test.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								test/source/scwx/provider/iem_api_provider.test.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,57 @@ | ||||||
|  | #include <scwx/provider/iem_api_provider.ipp> | ||||||
|  | 
 | ||||||
|  | #include <gtest/gtest.h> | ||||||
|  | 
 | ||||||
|  | namespace scwx::provider | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | TEST(IemApiProviderTest, ListTextProducts) | ||||||
|  | { | ||||||
|  |    using namespace std::chrono; | ||||||
|  |    using sys_days = time_point<system_clock, days>; | ||||||
|  | 
 | ||||||
|  |    auto date = sys_days {2023y / March / 25d}; | ||||||
|  | 
 | ||||||
|  |    auto torProducts = IemApiProvider::ListTextProducts(date, {}, "TOR"); | ||||||
|  | 
 | ||||||
|  |    ASSERT_EQ(torProducts.has_value(), true); | ||||||
|  |    EXPECT_EQ(torProducts.value().size(), 35); | ||||||
|  | 
 | ||||||
|  |    if (torProducts.value().size() >= 1) | ||||||
|  |    { | ||||||
|  |       EXPECT_EQ(torProducts.value().at(0).productId_, | ||||||
|  |                 "202303250016-KMEG-WFUS54-TORMEG"); | ||||||
|  |    } | ||||||
|  |    if (torProducts.value().size() >= 35) | ||||||
|  |    { | ||||||
|  |       EXPECT_EQ(torProducts.value().at(34).productId_, | ||||||
|  |                 "202303252015-KFFC-WFUS52-TORFFC"); | ||||||
|  |    } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | TEST(IemApiProviderTest, LoadTextProducts) | ||||||
|  | { | ||||||
|  |    static const std::vector<std::string> productIds { | ||||||
|  |       "202303250016-KMEG-WFUS54-TORMEG", | ||||||
|  |       "202303252015-KFFC-WFUS52-TORFFC", | ||||||
|  |       "202303311942-KLZK-WWUS54-SVSLZK"}; | ||||||
|  | 
 | ||||||
|  |    auto textProducts = IemApiProvider::LoadTextProducts(productIds); | ||||||
|  | 
 | ||||||
|  |    EXPECT_EQ(textProducts.size(), 3); | ||||||
|  | 
 | ||||||
|  |    if (textProducts.size() >= 1) | ||||||
|  |    { | ||||||
|  |       EXPECT_EQ(textProducts.at(0)->message_count(), 1); | ||||||
|  |    } | ||||||
|  |    if (textProducts.size() >= 2) | ||||||
|  |    { | ||||||
|  |       EXPECT_EQ(textProducts.at(1)->message_count(), 1); | ||||||
|  |    } | ||||||
|  |    if (textProducts.size() >= 3) | ||||||
|  |    { | ||||||
|  |       EXPECT_EQ(textProducts.at(2)->message_count(), 2); | ||||||
|  |    } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } // namespace scwx::provider
 | ||||||
|  | @ -13,53 +13,27 @@ static const std::string& kAlternateUrl {"https://warnings.cod.edu"}; | ||||||
| class WarningsProviderTest : public testing::TestWithParam<std::string> | class WarningsProviderTest : public testing::TestWithParam<std::string> | ||||||
| { | { | ||||||
| }; | }; | ||||||
| TEST_P(WarningsProviderTest, ListFiles) |  | ||||||
| { |  | ||||||
|    WarningsProvider provider(GetParam()); |  | ||||||
| 
 |  | ||||||
|    auto [newObjects, totalObjects] = provider.ListFiles(); |  | ||||||
| 
 |  | ||||||
|    // No objects, skip test
 |  | ||||||
|    if (totalObjects == 0) |  | ||||||
|    { |  | ||||||
|       GTEST_SKIP(); |  | ||||||
|    } |  | ||||||
| 
 |  | ||||||
|    EXPECT_GT(newObjects, 0); |  | ||||||
|    EXPECT_GT(totalObjects, 0); |  | ||||||
|    EXPECT_EQ(newObjects, totalObjects); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| TEST_P(WarningsProviderTest, LoadUpdatedFiles) | TEST_P(WarningsProviderTest, LoadUpdatedFiles) | ||||||
| { | { | ||||||
|    WarningsProvider provider(GetParam()); |    WarningsProvider provider(GetParam()); | ||||||
| 
 | 
 | ||||||
|    auto [newObjects, totalObjects] = provider.ListFiles(); |    const std::chrono::sys_time<std::chrono::hours> now = | ||||||
|    auto updatedFiles               = provider.LoadUpdatedFiles(); |       std::chrono::floor<std::chrono::hours>(std::chrono::system_clock::now()); | ||||||
|  |    const std::chrono::sys_time<std::chrono::hours> startTime = | ||||||
|  |       now - std::chrono::days {3}; | ||||||
|  | 
 | ||||||
|  |    auto updatedFiles = provider.LoadUpdatedFiles(startTime); | ||||||
| 
 | 
 | ||||||
|    // No objects, skip test
 |    // No objects, skip test
 | ||||||
|    if (totalObjects == 0) |    if (updatedFiles.empty()) | ||||||
|    { |    { | ||||||
|       GTEST_SKIP(); |       GTEST_SKIP(); | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|    EXPECT_GT(newObjects, 0); |    EXPECT_GT(updatedFiles.size(), 0); | ||||||
|    EXPECT_GT(totalObjects, 0); |  | ||||||
|    EXPECT_EQ(newObjects, totalObjects); |  | ||||||
|    EXPECT_EQ(updatedFiles.size(), newObjects); |  | ||||||
| 
 | 
 | ||||||
|    auto [newObjects2, totalObjects2] = provider.ListFiles(); |  | ||||||
|    auto updatedFiles2 = provider.LoadUpdatedFiles(); |    auto updatedFiles2 = provider.LoadUpdatedFiles(); | ||||||
| 
 |  | ||||||
|    // There should be no more than 2 updated warnings files since the last query
 |  | ||||||
|    // (assumption that the previous newest file was updated, and a new file was
 |  | ||||||
|    // created on the hour)
 |  | ||||||
|    EXPECT_LE(newObjects2, 2); |  | ||||||
|    EXPECT_EQ(updatedFiles2.size(), newObjects2); |  | ||||||
| 
 |  | ||||||
|    // The total number of objects may have changed, since the oldest file could
 |  | ||||||
|    // have dropped off the list
 |  | ||||||
|    EXPECT_GT(totalObjects2, 0); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| INSTANTIATE_TEST_SUITE_P(WarningsProvider, | INSTANTIATE_TEST_SUITE_P(WarningsProvider, | ||||||
|  |  | ||||||
|  | @ -12,13 +12,15 @@ set(SRC_AWIPS_TESTS source/scwx/awips/coded_location.test.cpp | ||||||
|                     source/scwx/awips/coded_time_motion_location.test.cpp |                     source/scwx/awips/coded_time_motion_location.test.cpp | ||||||
|                     source/scwx/awips/pvtec.test.cpp |                     source/scwx/awips/pvtec.test.cpp | ||||||
|                     source/scwx/awips/text_product_file.test.cpp |                     source/scwx/awips/text_product_file.test.cpp | ||||||
|                     source/scwx/awips/ugc.test.cpp) |                     source/scwx/awips/ugc.test.cpp | ||||||
|  |                     source/scwx/awips/wmo_header.test.cpp) | ||||||
| set(SRC_COMMON_TESTS source/scwx/common/color_table.test.cpp | set(SRC_COMMON_TESTS source/scwx/common/color_table.test.cpp | ||||||
|                      source/scwx/common/products.test.cpp) |                      source/scwx/common/products.test.cpp) | ||||||
| set(SRC_GR_TESTS source/scwx/gr/placefile.test.cpp) | set(SRC_GR_TESTS source/scwx/gr/placefile.test.cpp) | ||||||
| set(SRC_NETWORK_TESTS source/scwx/network/dir_list.test.cpp) | set(SRC_NETWORK_TESTS source/scwx/network/dir_list.test.cpp) | ||||||
| set(SRC_PROVIDER_TESTS source/scwx/provider/aws_level2_data_provider.test.cpp | set(SRC_PROVIDER_TESTS source/scwx/provider/aws_level2_data_provider.test.cpp | ||||||
|                        source/scwx/provider/aws_level3_data_provider.test.cpp |                        source/scwx/provider/aws_level3_data_provider.test.cpp | ||||||
|  |                        source/scwx/provider/iem_api_provider.test.cpp | ||||||
|                        source/scwx/provider/warnings_provider.test.cpp) |                        source/scwx/provider/warnings_provider.test.cpp) | ||||||
| set(SRC_QT_CONFIG_TESTS source/scwx/qt/config/county_database.test.cpp | set(SRC_QT_CONFIG_TESTS source/scwx/qt/config/county_database.test.cpp | ||||||
|                         source/scwx/qt/config/radar_site.test.cpp) |                         source/scwx/qt/config/radar_site.test.cpp) | ||||||
|  |  | ||||||
|  | @ -5,9 +5,7 @@ | ||||||
| #include <memory> | #include <memory> | ||||||
| #include <string> | #include <string> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx::awips | ||||||
| { |  | ||||||
| namespace awips |  | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| class TextProductFileImpl; | class TextProductFileImpl; | ||||||
|  | @ -24,16 +22,17 @@ public: | ||||||
|    TextProductFile(TextProductFile&&) noexcept; |    TextProductFile(TextProductFile&&) noexcept; | ||||||
|    TextProductFile& operator=(TextProductFile&&) noexcept; |    TextProductFile& operator=(TextProductFile&&) noexcept; | ||||||
| 
 | 
 | ||||||
|    size_t                                           message_count() const; |    [[nodiscard]] std::size_t message_count() const; | ||||||
|    std::vector<std::shared_ptr<TextProductMessage>> messages() const; |    [[nodiscard]] std::vector<std::shared_ptr<TextProductMessage>> | ||||||
|    std::shared_ptr<TextProductMessage>              message(size_t i) const; |                                                      messages() const; | ||||||
|  |    [[nodiscard]] std::shared_ptr<TextProductMessage> message(size_t i) const; | ||||||
| 
 | 
 | ||||||
|    bool LoadFile(const std::string& filename); |    bool LoadFile(const std::string& filename); | ||||||
|    bool LoadData(std::istream& is); |    bool LoadData(const std::string& filename, std::istream& is); | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|    std::unique_ptr<TextProductFileImpl> p; |    class Impl; | ||||||
|  |    std::unique_ptr<Impl> p; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| } // namespace awips
 | } // namespace scwx::awips
 | ||||||
| } // namespace scwx
 |  | ||||||
|  |  | ||||||
|  | @ -13,6 +13,8 @@ | ||||||
| #include <memory> | #include <memory> | ||||||
| #include <string> | #include <string> | ||||||
| 
 | 
 | ||||||
|  | #include <boost/uuid/uuid.hpp> | ||||||
|  | 
 | ||||||
| namespace scwx | namespace scwx | ||||||
| { | { | ||||||
| namespace awips | namespace awips | ||||||
|  | @ -94,6 +96,7 @@ public: | ||||||
|    TextProductMessage(TextProductMessage&&) noexcept; |    TextProductMessage(TextProductMessage&&) noexcept; | ||||||
|    TextProductMessage& operator=(TextProductMessage&&) noexcept; |    TextProductMessage& operator=(TextProductMessage&&) noexcept; | ||||||
| 
 | 
 | ||||||
|  |    [[nodiscard]] boost::uuids::uuid            uuid() const; | ||||||
|    std::string                                 message_content() const; |    std::string                                 message_content() const; | ||||||
|    std::shared_ptr<WmoHeader>                  wmo_header() const; |    std::shared_ptr<WmoHeader>                  wmo_header() const; | ||||||
|    std::vector<std::string>                    mnd_header() const; |    std::vector<std::string>                    mnd_header() const; | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| #pragma once | #pragma once | ||||||
| 
 | 
 | ||||||
|  | #include <chrono> | ||||||
| #include <memory> | #include <memory> | ||||||
|  | #include <optional> | ||||||
| #include <string> | #include <string> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx::awips | ||||||
| { |  | ||||||
| namespace awips |  | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| class WmoHeaderImpl; | class WmoHeaderImpl; | ||||||
|  | @ -35,21 +35,53 @@ public: | ||||||
| 
 | 
 | ||||||
|    bool operator==(const WmoHeader& o) const; |    bool operator==(const WmoHeader& o) const; | ||||||
| 
 | 
 | ||||||
|    std::string sequence_number() const; |    [[nodiscard]] std::string sequence_number() const; | ||||||
|    std::string data_type() const; |    [[nodiscard]] std::string data_type() const; | ||||||
|    std::string geographic_designator() const; |    [[nodiscard]] std::string geographic_designator() const; | ||||||
|    std::string bulletin_id() const; |    [[nodiscard]] std::string bulletin_id() const; | ||||||
|    std::string icao() const; |    [[nodiscard]] std::string icao() const; | ||||||
|    std::string date_time() const; |    [[nodiscard]] std::string date_time() const; | ||||||
|    std::string bbb_indicator() const; |    [[nodiscard]] std::string bbb_indicator() const; | ||||||
|    std::string product_category() const; |    [[nodiscard]] std::string product_category() const; | ||||||
|    std::string product_designator() const; |    [[nodiscard]] std::string product_designator() const; | ||||||
| 
 | 
 | ||||||
|  |    /**
 | ||||||
|  |     * @brief Get the WMO date/time | ||||||
|  |     * | ||||||
|  |     * Gets the WMO date/time. Uses the optional date hint provided via | ||||||
|  |     * SetDateHint(std::chrono::year_month). If the date hint has not been | ||||||
|  |     * provided, the endTimeHint parameter is required. | ||||||
|  |     * | ||||||
|  |     * @param [in] endTimeHint The optional end time bounds to provide. This is | ||||||
|  |     * ignored if a date hint has been provided to determine an absolute date. | ||||||
|  |     */ | ||||||
|  |    [[nodiscard]] std::chrono::sys_time<std::chrono::minutes> GetDateTime( | ||||||
|  |       std::optional<std::chrono::system_clock::time_point> endTimeHint = | ||||||
|  |          std::nullopt); | ||||||
|  | 
 | ||||||
|  |    /**
 | ||||||
|  |     * @brief Parse a WMO header | ||||||
|  |     * | ||||||
|  |     * @param [in] is The input stream to parse | ||||||
|  |     */ | ||||||
|    bool Parse(std::istream& is); |    bool Parse(std::istream& is); | ||||||
| 
 | 
 | ||||||
|  |    /**
 | ||||||
|  |     * @brief Provide a date hint for the WMO parser | ||||||
|  |     * | ||||||
|  |     * The WMO header contains a date/time in the format DDHHMM. The year and | ||||||
|  |     * month must be derived using another source. The date hint provides the | ||||||
|  |     * additional context required to determine the absolute product time. | ||||||
|  |     * | ||||||
|  |     * This function will update any absolute date/time already calculated, or | ||||||
|  |     * affect the calculation of a subsequent absolute date/time. | ||||||
|  |     * | ||||||
|  |     * @param [in] dateHint The date hint to provide the WMO header parser | ||||||
|  |     */ | ||||||
|  |    void SetDateHint(std::chrono::year_month dateHint); | ||||||
|  | 
 | ||||||
| private: | private: | ||||||
|    std::unique_ptr<WmoHeaderImpl> p; |    std::unique_ptr<WmoHeaderImpl> p; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| } // namespace awips
 | } // namespace scwx::awips
 | ||||||
| } // namespace scwx
 |  | ||||||
|  |  | ||||||
							
								
								
									
										80
									
								
								wxdata/include/scwx/provider/iem_api_provider.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								wxdata/include/scwx/provider/iem_api_provider.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,80 @@ | ||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include <scwx/awips/text_product_file.hpp> | ||||||
|  | #include <scwx/types/iem_types.hpp> | ||||||
|  | #include <scwx/util/logger.hpp> | ||||||
|  | 
 | ||||||
|  | #include <memory> | ||||||
|  | #include <string> | ||||||
|  | #include <vector> | ||||||
|  | 
 | ||||||
|  | #include <boost/outcome/result.hpp> | ||||||
|  | #include <cpr/session.h> | ||||||
|  | #include <range/v3/range/concepts.hpp> | ||||||
|  | 
 | ||||||
|  | #if defined(_MSC_VER) | ||||||
|  | #   pragma warning(push) | ||||||
|  | #   pragma warning(disable : 4702) | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | #if defined(_MSC_VER) | ||||||
|  | #   pragma warning(pop) | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | namespace scwx::provider | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Warnings Provider | ||||||
|  |  */ | ||||||
|  | class IemApiProvider | ||||||
|  | { | ||||||
|  | public: | ||||||
|  |    explicit IemApiProvider(); | ||||||
|  |    ~IemApiProvider(); | ||||||
|  | 
 | ||||||
|  |    IemApiProvider(const IemApiProvider&)            = delete; | ||||||
|  |    IemApiProvider& operator=(const IemApiProvider&) = delete; | ||||||
|  | 
 | ||||||
|  |    IemApiProvider(IemApiProvider&&) noexcept; | ||||||
|  |    IemApiProvider& operator=(IemApiProvider&&) noexcept; | ||||||
|  | 
 | ||||||
|  |    static boost::outcome_v2::result<std::vector<types::iem::AfosEntry>> | ||||||
|  |    ListTextProducts(std::chrono::sys_days           date, | ||||||
|  |                     std::optional<std::string_view> cccc = {}, | ||||||
|  |                     std::optional<std::string_view> pil  = {}); | ||||||
|  | 
 | ||||||
|  |    template<ranges::forward_range DateRange, | ||||||
|  |             ranges::forward_range CcccRange, | ||||||
|  |             ranges::forward_range PilRange> | ||||||
|  |       requires std::same_as<ranges::range_value_t<DateRange>, | ||||||
|  |                             std::chrono::sys_days> && | ||||||
|  |                std::same_as<ranges::range_value_t<CcccRange>, | ||||||
|  |                             std::string_view> && | ||||||
|  |                std::same_as<ranges::range_value_t<PilRange>, std::string_view> | ||||||
|  |    static boost::outcome_v2::result<std::vector<types::iem::AfosEntry>> | ||||||
|  |    ListTextProducts(DateRange dates, CcccRange ccccs, PilRange pils); | ||||||
|  | 
 | ||||||
|  |    template<ranges::forward_range Range> | ||||||
|  |       requires std::same_as<ranges::range_value_t<Range>, std::string> | ||||||
|  |    static std::vector<std::shared_ptr<awips::TextProductFile>> | ||||||
|  |    LoadTextProducts(const Range& textProducts); | ||||||
|  | 
 | ||||||
|  | private: | ||||||
|  |    class Impl; | ||||||
|  |    std::unique_ptr<Impl> p; | ||||||
|  | 
 | ||||||
|  |    static boost::outcome_v2::result<std::vector<types::iem::AfosEntry>> | ||||||
|  |    ProcessTextProductLists(std::vector<cpr::AsyncResponse>& asyncResponses); | ||||||
|  |    static std::vector<std::shared_ptr<awips::TextProductFile>> | ||||||
|  |    ProcessTextProductFiles( | ||||||
|  |       std::vector<std::pair<std::string, cpr::AsyncResponse>>& asyncResponses); | ||||||
|  | 
 | ||||||
|  |    static const std::shared_ptr<spdlog::logger> logger_; | ||||||
|  | 
 | ||||||
|  |    static const std::string kBaseUrl_; | ||||||
|  |    static const std::string kListNwsTextProductsEndpoint_; | ||||||
|  |    static const std::string kNwsTextProductEndpoint_; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | } // namespace scwx::provider
 | ||||||
							
								
								
									
										111
									
								
								wxdata/include/scwx/provider/iem_api_provider.ipp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								wxdata/include/scwx/provider/iem_api_provider.ipp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,111 @@ | ||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include <scwx/provider/iem_api_provider.hpp> | ||||||
|  | #include <scwx/network/cpr.hpp> | ||||||
|  | #include <scwx/util/time.hpp> | ||||||
|  | 
 | ||||||
|  | #include <boost/algorithm/string/join.hpp> | ||||||
|  | #include <cpr/cpr.h> | ||||||
|  | #include <range/v3/view/cartesian_product.hpp> | ||||||
|  | #include <range/v3/view/single.hpp> | ||||||
|  | #include <range/v3/view/transform.hpp> | ||||||
|  | 
 | ||||||
|  | #if (__cpp_lib_chrono < 201907L) | ||||||
|  | #   include <date/date.h> | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | namespace scwx::provider | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | template<ranges::forward_range DateRange, | ||||||
|  |          ranges::forward_range CcccRange, | ||||||
|  |          ranges::forward_range PilRange> | ||||||
|  |    requires std::same_as<ranges::range_value_t<DateRange>, | ||||||
|  |                          std::chrono::sys_days> && | ||||||
|  |             std::same_as<ranges::range_value_t<CcccRange>, std::string_view> && | ||||||
|  |             std::same_as<ranges::range_value_t<PilRange>, std::string_view> | ||||||
|  | boost::outcome_v2::result<std::vector<types::iem::AfosEntry>> | ||||||
|  | IemApiProvider::ListTextProducts(DateRange dates, | ||||||
|  |                                  CcccRange ccccs, | ||||||
|  |                                  PilRange  pils) | ||||||
|  | { | ||||||
|  |    using namespace std::chrono; | ||||||
|  | 
 | ||||||
|  | #if (__cpp_lib_chrono >= 201907L) | ||||||
|  |    namespace df = std; | ||||||
|  | 
 | ||||||
|  |    static constexpr std::string_view kDateFormat {"{:%Y-%m-%d}"}; | ||||||
|  | #else | ||||||
|  |    using namespace date; | ||||||
|  |    namespace df = date; | ||||||
|  | 
 | ||||||
|  | #   define kDateFormat "%Y-%m-%d" | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  |    auto formattedDates = dates | ranges::views::transform( | ||||||
|  |                                     [](const std::chrono::sys_days& date) | ||||||
|  |                                     { return df::format(kDateFormat, date); }); | ||||||
|  | 
 | ||||||
|  |    logger_->debug("Listing text products for: {}", | ||||||
|  |                   boost::algorithm::join(formattedDates, ", ")); | ||||||
|  | 
 | ||||||
|  |    std::vector<cpr::AsyncResponse> asyncResponses {}; | ||||||
|  | 
 | ||||||
|  |    for (const auto& [date, cccc, pil] : | ||||||
|  |         ranges::views::cartesian_product(dates, ccccs, pils)) | ||||||
|  |    { | ||||||
|  |       auto parameters = | ||||||
|  |          cpr::Parameters {{"date", df::format(kDateFormat, date)}}; | ||||||
|  | 
 | ||||||
|  |       // WMO Source Code | ||||||
|  |       if (!cccc.empty()) | ||||||
|  |       { | ||||||
|  |          parameters.Add({"cccc", std::string {cccc}}); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // AFOS / AWIPS ID / 3-6 length identifier | ||||||
|  |       if (!pil.empty()) | ||||||
|  |       { | ||||||
|  |          parameters.Add({"pil", std::string {pil}}); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       asyncResponses.emplace_back( | ||||||
|  |          cpr::GetAsync(cpr::Url {kBaseUrl_ + kListNwsTextProductsEndpoint_}, | ||||||
|  |                        network::cpr::GetHeader(), | ||||||
|  |                        parameters)); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    return ProcessTextProductLists(asyncResponses); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | template<ranges::forward_range Range> | ||||||
|  |    requires std::same_as<ranges::range_value_t<Range>, std::string> | ||||||
|  | std::vector<std::shared_ptr<awips::TextProductFile>> | ||||||
|  | IemApiProvider::LoadTextProducts(const Range& textProducts) | ||||||
|  | { | ||||||
|  |    auto parameters = cpr::Parameters {{"nolimit", "true"}}; | ||||||
|  | 
 | ||||||
|  |    logger_->debug("Loading {} text products", textProducts.size()); | ||||||
|  | 
 | ||||||
|  |    std::vector<std::pair<std::string, cpr::AsyncResponse>> asyncResponses {}; | ||||||
|  |    asyncResponses.reserve(textProducts.size()); | ||||||
|  | 
 | ||||||
|  |    const std::string endpointUrl = kBaseUrl_ + kNwsTextProductEndpoint_; | ||||||
|  | 
 | ||||||
|  |    for (const auto& productId : textProducts) | ||||||
|  |    { | ||||||
|  |       asyncResponses.emplace_back( | ||||||
|  |          productId, | ||||||
|  |          cpr::GetAsync(cpr::Url {endpointUrl + productId}, | ||||||
|  |                        network::cpr::GetHeader(), | ||||||
|  |                        parameters)); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    return ProcessTextProductFiles(asyncResponses); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #ifdef kDateFormat | ||||||
|  | #   undef kDateFormat | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | } // namespace scwx::provider | ||||||
|  | @ -2,9 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| #include <scwx/awips/text_product_file.hpp> | #include <scwx/awips/text_product_file.hpp> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx::provider | ||||||
| { |  | ||||||
| namespace provider |  | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| /**
 | /**
 | ||||||
|  | @ -22,15 +20,12 @@ public: | ||||||
|    WarningsProvider(WarningsProvider&&) noexcept; |    WarningsProvider(WarningsProvider&&) noexcept; | ||||||
|    WarningsProvider& operator=(WarningsProvider&&) noexcept; |    WarningsProvider& operator=(WarningsProvider&&) noexcept; | ||||||
| 
 | 
 | ||||||
|    std::pair<size_t, size_t> |  | ||||||
|    ListFiles(std::chrono::system_clock::time_point newerThan = {}); |  | ||||||
|    std::vector<std::shared_ptr<awips::TextProductFile>> |    std::vector<std::shared_ptr<awips::TextProductFile>> | ||||||
|    LoadUpdatedFiles(std::chrono::system_clock::time_point newerThan = {}); |    LoadUpdatedFiles(std::chrono::sys_time<std::chrono::hours> newerThan = {}); | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|    class Impl; |    class Impl; | ||||||
|    std::unique_ptr<Impl> p; |    std::unique_ptr<Impl> p; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| } // namespace provider
 | } // namespace scwx::provider
 | ||||||
| } // namespace scwx
 |  | ||||||
|  |  | ||||||
							
								
								
									
										73
									
								
								wxdata/include/scwx/types/iem_types.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								wxdata/include/scwx/types/iem_types.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,73 @@ | ||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include <string> | ||||||
|  | #include <variant> | ||||||
|  | 
 | ||||||
|  | #include <boost/json/value.hpp> | ||||||
|  | 
 | ||||||
|  | namespace scwx::types::iem | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief AFOS Entry object | ||||||
|  |  * | ||||||
|  |  * <https://mesonet.agron.iastate.edu/api/1/docs#/nws/service_nws_afos_list__fmt__get>
 | ||||||
|  |  */ | ||||||
|  | struct AfosEntry | ||||||
|  | { | ||||||
|  |    std::int64_t index_ {}; | ||||||
|  |    std::string  entered_ {}; | ||||||
|  |    std::string  pil_ {}; | ||||||
|  |    std::string  productId_ {}; | ||||||
|  |    std::string  cccc_ {}; | ||||||
|  |    std::int64_t count_ {}; | ||||||
|  |    std::string  link_ {}; | ||||||
|  |    std::string  textLink_ {}; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief AFOS List object | ||||||
|  |  * | ||||||
|  |  * <https://mesonet.agron.iastate.edu/api/1/docs#/nws/service_nws_afos_list__fmt__get>
 | ||||||
|  |  */ | ||||||
|  | struct AfosList | ||||||
|  | { | ||||||
|  |    std::vector<AfosEntry> data_ {}; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Bad Request (400) object | ||||||
|  |  */ | ||||||
|  | struct BadRequest | ||||||
|  | { | ||||||
|  |    std::string detail_ {}; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * @brief Validation Error (422) object | ||||||
|  |  */ | ||||||
|  | struct ValidationError | ||||||
|  | { | ||||||
|  |    struct Detail | ||||||
|  |    { | ||||||
|  |       std::string                                          type_ {}; | ||||||
|  |       std::vector<std::variant<std::int64_t, std::string>> loc_ {}; | ||||||
|  |       std::string                                          msg_ {}; | ||||||
|  |       std::string                                          input_ {}; | ||||||
|  |       struct Context | ||||||
|  |       { | ||||||
|  |          std::string error_ {}; | ||||||
|  |       } ctx_; | ||||||
|  |    }; | ||||||
|  | 
 | ||||||
|  |    std::vector<Detail> detail_ {}; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | AfosList        tag_invoke(boost::json::value_to_tag<AfosList>, | ||||||
|  |                            const boost::json::value& jv); | ||||||
|  | BadRequest      tag_invoke(boost::json::value_to_tag<BadRequest>, | ||||||
|  |                            const boost::json::value& jv); | ||||||
|  | ValidationError tag_invoke(boost::json::value_to_tag<ValidationError>, | ||||||
|  |                            const boost::json::value& jv); | ||||||
|  | 
 | ||||||
|  | } // namespace scwx::types::iem
 | ||||||
							
								
								
									
										15
									
								
								wxdata/include/scwx/util/json.hpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								wxdata/include/scwx/util/json.hpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | ||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include <boost/json/value.hpp> | ||||||
|  | 
 | ||||||
|  | namespace scwx::util::json | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | boost::json::value ReadJsonFile(const std::string& path); | ||||||
|  | boost::json::value ReadJsonStream(std::istream& is); | ||||||
|  | boost::json::value ReadJsonString(std::string_view sv); | ||||||
|  | void               WriteJsonFile(const std::string&        path, | ||||||
|  |                                  const boost::json::value& json, | ||||||
|  |                                  bool                      prettyPrint = true); | ||||||
|  | 
 | ||||||
|  | } // namespace scwx::util::json
 | ||||||
|  | @ -3,24 +3,31 @@ | ||||||
| 
 | 
 | ||||||
| #include <fstream> | #include <fstream> | ||||||
| 
 | 
 | ||||||
| namespace scwx | #include <re2/re2.h> | ||||||
| { | 
 | ||||||
| namespace awips | namespace scwx::awips | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| static const std::string logPrefix_ = "scwx::awips::text_product_file"; | static const std::string logPrefix_ = "scwx::awips::text_product_file"; | ||||||
| static const auto        logger_    = util::Logger::Create(logPrefix_); | static const auto        logger_    = util::Logger::Create(logPrefix_); | ||||||
| 
 | 
 | ||||||
| class TextProductFileImpl | class TextProductFile::Impl | ||||||
| { | { | ||||||
| public: | public: | ||||||
|    explicit TextProductFileImpl() : messages_ {} {}; |    explicit Impl() : messages_ {} {}; | ||||||
|    ~TextProductFileImpl() = default; |    ~Impl() = default; | ||||||
|  | 
 | ||||||
|  |    Impl(const Impl&)            = delete; | ||||||
|  |    Impl& operator=(const Impl&) = delete; | ||||||
|  | 
 | ||||||
|  |    Impl(Impl&&)            = delete; | ||||||
|  |    Impl& operator=(Impl&&) = delete; | ||||||
| 
 | 
 | ||||||
|    std::vector<std::shared_ptr<TextProductMessage>> messages_; |    std::vector<std::shared_ptr<TextProductMessage>> messages_; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| TextProductFile::TextProductFile() : p(std::make_unique<TextProductFileImpl>()) | TextProductFile::TextProductFile() : | ||||||
|  |     p(std::make_unique<TextProductFile::Impl>()) | ||||||
| { | { | ||||||
| } | } | ||||||
| TextProductFile::~TextProductFile() = default; | TextProductFile::~TextProductFile() = default; | ||||||
|  | @ -59,16 +66,34 @@ bool TextProductFile::LoadFile(const std::string& filename) | ||||||
| 
 | 
 | ||||||
|    if (fileValid) |    if (fileValid) | ||||||
|    { |    { | ||||||
|       fileValid = LoadData(f); |       fileValid = LoadData(filename, f); | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|    return fileValid; |    return fileValid; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| bool TextProductFile::LoadData(std::istream& is) | bool TextProductFile::LoadData(const std::string& filename, std::istream& is) | ||||||
| { | { | ||||||
|  |    static constexpr LazyRE2 kDateTimePattern_ = { | ||||||
|  |       R"(((?:19|20)\d{2}))"      // Year (YYYY)
 | ||||||
|  |       R"((0[1-9]|1[0-2]))"       // Month (MM)
 | ||||||
|  |       R"((0[1-9]|[12]\d|3[01]))" // Day (DD)
 | ||||||
|  |       R"(_?)"                    // Optional separator (not captured)
 | ||||||
|  |       R"(([01]\d|2[0-3]))"       // Hour (HH)
 | ||||||
|  |    }; | ||||||
|  | 
 | ||||||
|    logger_->trace("Loading Data"); |    logger_->trace("Loading Data"); | ||||||
| 
 | 
 | ||||||
|  |    // Attempt to parse the date from the filename
 | ||||||
|  |    std::optional<std::chrono::year_month> yearMonth; | ||||||
|  |    int                                    year {}; | ||||||
|  |    unsigned int                           month {}; | ||||||
|  | 
 | ||||||
|  |    if (RE2::PartialMatch(filename, *kDateTimePattern_, &year, &month)) | ||||||
|  |    { | ||||||
|  |       yearMonth = std::chrono::year {year} / std::chrono::month {month}; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|    while (!is.eof()) |    while (!is.eof()) | ||||||
|    { |    { | ||||||
|       std::shared_ptr<TextProductMessage> message = |       std::shared_ptr<TextProductMessage> message = | ||||||
|  | @ -77,7 +102,7 @@ bool TextProductFile::LoadData(std::istream& is) | ||||||
| 
 | 
 | ||||||
|       if (message != nullptr) |       if (message != nullptr) | ||||||
|       { |       { | ||||||
|          for (auto m : p->messages_) |          for (const auto& m : p->messages_) | ||||||
|          { |          { | ||||||
|             if (*m->wmo_header().get() == *message->wmo_header().get()) |             if (*m->wmo_header().get() == *message->wmo_header().get()) | ||||||
|             { |             { | ||||||
|  | @ -88,6 +113,11 @@ bool TextProductFile::LoadData(std::istream& is) | ||||||
| 
 | 
 | ||||||
|          if (!duplicate) |          if (!duplicate) | ||||||
|          { |          { | ||||||
|  |             if (yearMonth.has_value()) | ||||||
|  |             { | ||||||
|  |                message->wmo_header()->SetDateHint(yearMonth.value()); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             p->messages_.push_back(message); |             p->messages_.push_back(message); | ||||||
|          } |          } | ||||||
|       } |       } | ||||||
|  | @ -100,5 +130,4 @@ bool TextProductFile::LoadData(std::istream& is) | ||||||
|    return !p->messages_.empty(); |    return !p->messages_.empty(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| } // namespace awips
 | } // namespace scwx::awips
 | ||||||
| } // namespace scwx
 |  | ||||||
|  |  | ||||||
|  | @ -9,11 +9,10 @@ | ||||||
| 
 | 
 | ||||||
| #include <boost/algorithm/string/replace.hpp> | #include <boost/algorithm/string/replace.hpp> | ||||||
| #include <boost/algorithm/string/trim.hpp> | #include <boost/algorithm/string/trim.hpp> | ||||||
|  | #include <boost/uuid/random_generator.hpp> | ||||||
| #include <re2/re2.h> | #include <re2/re2.h> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx::awips | ||||||
| { |  | ||||||
| namespace awips |  | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| static const std::string logPrefix_ = "scwx::awips::text_product_message"; | static const std::string logPrefix_ = "scwx::awips::text_product_message"; | ||||||
|  | @ -27,7 +26,7 @@ static const auto        logger_    = scwx::util::Logger::Create(logPrefix_); | ||||||
| // Look for hhmm (xM|UTC) to key the date/time string
 | // Look for hhmm (xM|UTC) to key the date/time string
 | ||||||
| static constexpr LazyRE2 reDateTimeString = {"^[0-9]{3,4} ([AP]M|UTC)"}; | static constexpr LazyRE2 reDateTimeString = {"^[0-9]{3,4} ([AP]M|UTC)"}; | ||||||
| 
 | 
 | ||||||
| static void ParseCodedInformation(std::shared_ptr<Segment> segment, | static void ParseCodedInformation(const std::shared_ptr<Segment>& segment, | ||||||
|                                   const std::string&              wfo); |                                   const std::string&              wfo); | ||||||
| static std::vector<std::string>     ParseProductContent(std::istream& is); | static std::vector<std::string>     ParseProductContent(std::istream& is); | ||||||
| static void                         SkipBlankLines(std::istream& is); | static void                         SkipBlankLines(std::istream& is); | ||||||
|  | @ -50,6 +49,13 @@ public: | ||||||
|    } |    } | ||||||
|    ~TextProductMessageImpl() = default; |    ~TextProductMessageImpl() = default; | ||||||
| 
 | 
 | ||||||
|  |    TextProductMessageImpl(const TextProductMessageImpl&)             = delete; | ||||||
|  |    TextProductMessageImpl& operator=(const TextProductMessageImpl&)  = delete; | ||||||
|  |    TextProductMessageImpl(const TextProductMessageImpl&&)            = delete; | ||||||
|  |    TextProductMessageImpl& operator=(const TextProductMessageImpl&&) = delete; | ||||||
|  | 
 | ||||||
|  |    boost::uuids::uuid uuid_ {boost::uuids::random_generator()()}; | ||||||
|  | 
 | ||||||
|    std::string                           messageContent_; |    std::string                           messageContent_; | ||||||
|    std::shared_ptr<WmoHeader>            wmoHeader_; |    std::shared_ptr<WmoHeader>            wmoHeader_; | ||||||
|    std::vector<std::string>              mndHeader_; |    std::vector<std::string>              mndHeader_; | ||||||
|  | @ -67,6 +73,11 @@ TextProductMessage::TextProductMessage(TextProductMessage&&) noexcept = default; | ||||||
| TextProductMessage& | TextProductMessage& | ||||||
| TextProductMessage::operator=(TextProductMessage&&) noexcept = default; | TextProductMessage::operator=(TextProductMessage&&) noexcept = default; | ||||||
| 
 | 
 | ||||||
|  | boost::uuids::uuid TextProductMessage::uuid() const | ||||||
|  | { | ||||||
|  |    return p->uuid_; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| std::string TextProductMessage::message_content() const | std::string TextProductMessage::message_content() const | ||||||
| { | { | ||||||
|    return p->messageContent_; |    return p->messageContent_; | ||||||
|  | @ -116,71 +127,11 @@ std::chrono::system_clock::time_point Segment::event_begin() const | ||||||
|       // If event begin is 000000T0000Z
 |       // If event begin is 000000T0000Z
 | ||||||
|       if (eventBegin == std::chrono::system_clock::time_point {}) |       if (eventBegin == std::chrono::system_clock::time_point {}) | ||||||
|       { |       { | ||||||
|          using namespace std::chrono; |  | ||||||
| 
 |  | ||||||
|          // Determine event end from P-VTEC string
 |          // Determine event end from P-VTEC string
 | ||||||
|          system_clock::time_point eventEnd = |          std::chrono::system_clock::time_point eventEnd = | ||||||
|             header_->vtecString_[0].pVtec_.event_end(); |             header_->vtecString_[0].pVtec_.event_end(); | ||||||
| 
 | 
 | ||||||
|          auto           endDays = floor<days>(eventEnd); |          eventBegin = wmoHeader_->GetDateTime(eventEnd); | ||||||
|          year_month_day endDate {endDays}; |  | ||||||
| 
 |  | ||||||
|          // Determine WMO date/time
 |  | ||||||
|          std::string wmoDateTime = wmoHeader_->date_time(); |  | ||||||
| 
 |  | ||||||
|          bool          wmoDateTimeValid = false; |  | ||||||
|          unsigned int  dayOfMonth       = 0; |  | ||||||
|          unsigned long beginHour        = 0; |  | ||||||
|          unsigned long beginMinute      = 0; |  | ||||||
| 
 |  | ||||||
|          try |  | ||||||
|          { |  | ||||||
|             // WMO date time is in the format DDHHMM
 |  | ||||||
|             dayOfMonth = |  | ||||||
|                static_cast<unsigned int>(std::stoul(wmoDateTime.substr(0, 2))); |  | ||||||
|             beginHour        = std::stoul(wmoDateTime.substr(2, 2)); |  | ||||||
|             beginMinute      = std::stoul(wmoDateTime.substr(4, 2)); |  | ||||||
|             wmoDateTimeValid = true; |  | ||||||
|          } |  | ||||||
|          catch (const std::exception&) |  | ||||||
|          { |  | ||||||
|             logger_->warn("Malformed WMO date/time: {}", wmoDateTime); |  | ||||||
|          } |  | ||||||
| 
 |  | ||||||
|          if (wmoDateTimeValid) |  | ||||||
|          { |  | ||||||
|             // Combine end date year and month with WMO date time
 |  | ||||||
|             eventBegin = |  | ||||||
|                sys_days {endDate.year() / endDate.month() / day {dayOfMonth}} + |  | ||||||
|                hours {beginHour} + minutes {beginMinute}; |  | ||||||
| 
 |  | ||||||
|             // If the begin date is after the end date, assume the start time
 |  | ||||||
|             // was the previous month (give a 1 day grace period for expiring
 |  | ||||||
|             // events in the past)
 |  | ||||||
|             if (eventBegin > eventEnd + 24h) |  | ||||||
|             { |  | ||||||
|                // If the current end month is January
 |  | ||||||
|                if (endDate.month() == January) |  | ||||||
|                { |  | ||||||
|                   // The begin month must be December of last year
 |  | ||||||
|                   eventBegin = |  | ||||||
|                      sys_days { |  | ||||||
|                         year {static_cast<int>((endDate.year() - 1y).count())} / |  | ||||||
|                         December / day {dayOfMonth}} + |  | ||||||
|                      hours {beginHour} + minutes {beginMinute}; |  | ||||||
|                } |  | ||||||
|                else |  | ||||||
|                { |  | ||||||
|                   // Back up one month
 |  | ||||||
|                   eventBegin = |  | ||||||
|                      sys_days {endDate.year() / |  | ||||||
|                                month {static_cast<unsigned int>( |  | ||||||
|                                   (endDate.month() - month {1}).count())} / |  | ||||||
|                                day {dayOfMonth}} + |  | ||||||
|                      hours {beginHour} + minutes {beginMinute}; |  | ||||||
|                } |  | ||||||
|             } |  | ||||||
|          } |  | ||||||
|       } |       } | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|  | @ -232,7 +183,7 @@ bool TextProductMessage::Parse(std::istream& is) | ||||||
| 
 | 
 | ||||||
|       if (i == 0) |       if (i == 0) | ||||||
|       { |       { | ||||||
|          if (is.peek() != '\r') |          if (is.peek() != '\r' && is.peek() != '\n') | ||||||
|          { |          { | ||||||
|             segment->header_ = TryParseSegmentHeader(is); |             segment->header_ = TryParseSegmentHeader(is); | ||||||
|          } |          } | ||||||
|  | @ -318,7 +269,7 @@ bool TextProductMessage::Parse(std::istream& is) | ||||||
|    return dataValid; |    return dataValid; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void ParseCodedInformation(std::shared_ptr<Segment> segment, | void ParseCodedInformation(const std::shared_ptr<Segment>& segment, | ||||||
|                            const std::string&              wfo) |                            const std::string&              wfo) | ||||||
| { | { | ||||||
|    typedef std::vector<std::string>::const_iterator StringIterator; |    typedef std::vector<std::string>::const_iterator StringIterator; | ||||||
|  | @ -352,7 +303,7 @@ void ParseCodedInformation(std::shared_ptr<Segment> segment, | ||||||
|          codedLocationEnd = it; |          codedLocationEnd = it; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       else if (codedMotionBegin == productContent.cend() && |       if (codedMotionBegin == productContent.cend() && | ||||||
|           it->starts_with("TIME...MOT...LOC")) |           it->starts_with("TIME...MOT...LOC")) | ||||||
|       { |       { | ||||||
|          codedMotionBegin = it; |          codedMotionBegin = it; | ||||||
|  | @ -366,8 +317,7 @@ void ParseCodedInformation(std::shared_ptr<Segment> segment, | ||||||
|          codedMotionEnd = it; |          codedMotionEnd = it; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       else if (!segment->observed_ && |       if (!segment->observed_ && it->find("...OBSERVED") != std::string::npos) | ||||||
|                it->find("...OBSERVED") != std::string::npos) |  | ||||||
|       { |       { | ||||||
|          segment->observed_ = true; |          segment->observed_ = true; | ||||||
|       } |       } | ||||||
|  | @ -378,6 +328,8 @@ void ParseCodedInformation(std::shared_ptr<Segment> segment, | ||||||
|          segment->tornadoPossible_ = true; |          segment->tornadoPossible_ = true; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       // Assignment of an iterator permitted
 | ||||||
|  |       // NOLINTBEGIN(bugprone-assignment-in-if-condition)
 | ||||||
|       else if (segment->threatCategory_ == ibw::ThreatCategory::Base && |       else if (segment->threatCategory_ == ibw::ThreatCategory::Base && | ||||||
|                (threatTagIt = std::find_if(kThreatCategoryTags.cbegin(), |                (threatTagIt = std::find_if(kThreatCategoryTags.cbegin(), | ||||||
|                                            kThreatCategoryTags.cend(), |                                            kThreatCategoryTags.cend(), | ||||||
|  | @ -385,6 +337,7 @@ void ParseCodedInformation(std::shared_ptr<Segment> segment, | ||||||
|                                               return it->starts_with(tag); |                                               return it->starts_with(tag); | ||||||
|                                            })) != kThreatCategoryTags.cend() && |                                            })) != kThreatCategoryTags.cend() && | ||||||
|                it->length() > threatTagIt->length()) |                it->length() > threatTagIt->length()) | ||||||
|  |       // NOLINTEND(bugprone-assignment-in-if-condition)
 | ||||||
|       { |       { | ||||||
|          const std::string threatCategoryName = |          const std::string threatCategoryName = | ||||||
|             it->substr(threatTagIt->length()); |             it->substr(threatTagIt->length()); | ||||||
|  | @ -458,7 +411,7 @@ void SkipBlankLines(std::istream& is) | ||||||
| { | { | ||||||
|    std::string line; |    std::string line; | ||||||
| 
 | 
 | ||||||
|    while (is.peek() == '\r') |    while (is.peek() == '\r' || is.peek() == '\n') | ||||||
|    { |    { | ||||||
|       util::getline(is, line); |       util::getline(is, line); | ||||||
|    } |    } | ||||||
|  | @ -513,7 +466,7 @@ std::vector<std::string> TryParseMndHeader(std::istream& is) | ||||||
|    std::string              line; |    std::string              line; | ||||||
|    std::streampos           isBegin = is.tellg(); |    std::streampos           isBegin = is.tellg(); | ||||||
| 
 | 
 | ||||||
|    while (!is.eof() && is.peek() != '\r') |    while (!is.eof() && is.peek() != '\r' && is.peek() != '\n') | ||||||
|    { |    { | ||||||
|       util::getline(is, line); |       util::getline(is, line); | ||||||
|       mndHeader.push_back(line); |       mndHeader.push_back(line); | ||||||
|  | @ -546,7 +499,7 @@ std::vector<std::string> TryParseOverviewBlock(std::istream& is) | ||||||
| 
 | 
 | ||||||
|    if (is.peek() == '.') |    if (is.peek() == '.') | ||||||
|    { |    { | ||||||
|       while (!is.eof() && is.peek() != '\r') |       while (!is.eof() && is.peek() != '\r' && is.peek() != '\n') | ||||||
|       { |       { | ||||||
|          util::getline(is, line); |          util::getline(is, line); | ||||||
|          overviewBlock.push_back(line); |          overviewBlock.push_back(line); | ||||||
|  | @ -576,7 +529,7 @@ std::optional<SegmentHeader> TryParseSegmentHeader(std::istream& is) | ||||||
|       header->ugcString_.push_back(line); |       header->ugcString_.push_back(line); | ||||||
| 
 | 
 | ||||||
|       // If UGC is multi-line, continue parsing
 |       // If UGC is multi-line, continue parsing
 | ||||||
|       while (!is.eof() && is.peek() != '\r' && |       while (!is.eof() && is.peek() != '\r' && is.peek() != '\n' && | ||||||
|              !RE2::PartialMatch(line, *reUgcExpiration)) |              !RE2::PartialMatch(line, *reUgcExpiration)) | ||||||
|       { |       { | ||||||
|          util::getline(is, line); |          util::getline(is, line); | ||||||
|  | @ -595,7 +548,7 @@ std::optional<SegmentHeader> TryParseSegmentHeader(std::istream& is) | ||||||
|          header->vtecString_.push_back(std::move(*vtec)); |          header->vtecString_.push_back(std::move(*vtec)); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       while (!is.eof() && is.peek() != '\r') |       while (!is.eof() && is.peek() != '\r' && is.peek() != '\n') | ||||||
|       { |       { | ||||||
|          util::getline(is, line); |          util::getline(is, line); | ||||||
|          if (!RE2::PartialMatch(line, *reDateTimeString)) |          if (!RE2::PartialMatch(line, *reDateTimeString)) | ||||||
|  | @ -640,10 +593,8 @@ std::optional<Vtec> TryParseVtecString(std::istream& is) | ||||||
| 
 | 
 | ||||||
|    if (RE2::PartialMatch(line, *rePVtecString)) |    if (RE2::PartialMatch(line, *rePVtecString)) | ||||||
|    { |    { | ||||||
|       bool vtecValid; |  | ||||||
| 
 |  | ||||||
|       vtec                 = Vtec(); |       vtec                 = Vtec(); | ||||||
|       vtecValid = vtec->pVtec_.Parse(line); |       const bool vtecValid = vtec->pVtec_.Parse(line); | ||||||
| 
 | 
 | ||||||
|       isBegin = is.tellg(); |       isBegin = is.tellg(); | ||||||
| 
 | 
 | ||||||
|  | @ -687,5 +638,4 @@ std::shared_ptr<TextProductMessage> TextProductMessage::Create(std::istream& is) | ||||||
|    return message; |    return message; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| } // namespace awips
 | } // namespace scwx::awips
 | ||||||
| } // namespace scwx
 |  | ||||||
|  |  | ||||||
|  | @ -12,14 +12,19 @@ | ||||||
| #   include <arpa/inet.h> | #   include <arpa/inet.h> | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx::awips | ||||||
| { |  | ||||||
| namespace awips |  | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| static const std::string logPrefix_ = "scwx::awips::wmo_header"; | static const std::string logPrefix_ = "scwx::awips::wmo_header"; | ||||||
| static const auto        logger_    = util::Logger::Create(logPrefix_); | static const auto        logger_    = util::Logger::Create(logPrefix_); | ||||||
| 
 | 
 | ||||||
|  | static constexpr std::size_t kWmoHeaderMinLineLength_    = 18; | ||||||
|  | static constexpr std::size_t kWmoIdentifierLengthMin_    = 5; | ||||||
|  | static constexpr std::size_t kWmoIdentifierLengthMax_    = 6; | ||||||
|  | static constexpr std::size_t kIcaoLength_                = 4; | ||||||
|  | static constexpr std::size_t kDateTimeLength_            = 6; | ||||||
|  | static constexpr std::size_t kAwipsIdentifierLineLength_ = 6; | ||||||
|  | 
 | ||||||
| class WmoHeaderImpl | class WmoHeaderImpl | ||||||
| { | { | ||||||
| public: | public: | ||||||
|  | @ -37,17 +42,31 @@ public: | ||||||
|    } |    } | ||||||
|    ~WmoHeaderImpl() = default; |    ~WmoHeaderImpl() = default; | ||||||
| 
 | 
 | ||||||
|  |    WmoHeaderImpl(const WmoHeaderImpl&)             = delete; | ||||||
|  |    WmoHeaderImpl& operator=(const WmoHeaderImpl&)  = delete; | ||||||
|  |    WmoHeaderImpl(const WmoHeaderImpl&&)            = delete; | ||||||
|  |    WmoHeaderImpl& operator=(const WmoHeaderImpl&&) = delete; | ||||||
|  | 
 | ||||||
|  |    void CalculateAbsoluteDateTime(); | ||||||
|  |    bool ParseDateTime(unsigned int&  dayOfMonth, | ||||||
|  |                       unsigned long& hour, | ||||||
|  |                       unsigned long& minute); | ||||||
|  | 
 | ||||||
|    bool operator==(const WmoHeaderImpl& o) const; |    bool operator==(const WmoHeaderImpl& o) const; | ||||||
| 
 | 
 | ||||||
|    std::string sequenceNumber_; |    std::string sequenceNumber_ {}; | ||||||
|    std::string dataType_; |    std::string dataType_ {}; | ||||||
|    std::string geographicDesignator_; |    std::string geographicDesignator_ {}; | ||||||
|    std::string bulletinId_; |    std::string bulletinId_ {}; | ||||||
|    std::string icao_; |    std::string icao_ {}; | ||||||
|    std::string dateTime_; |    std::string dateTime_ {}; | ||||||
|    std::string bbbIndicator_; |    std::string bbbIndicator_ {}; | ||||||
|    std::string productCategory_; |    std::string productCategory_ {}; | ||||||
|    std::string productDesignator_; |    std::string productDesignator_ {}; | ||||||
|  | 
 | ||||||
|  |    std::optional<std::chrono::year_month> dateHint_ {}; | ||||||
|  |    std::optional<std::chrono::sys_time<std::chrono::minutes>> | ||||||
|  |       absoluteDateTime_ {}; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| WmoHeader::WmoHeader() : p(std::make_unique<WmoHeaderImpl>()) {} | WmoHeader::WmoHeader() : p(std::make_unique<WmoHeaderImpl>()) {} | ||||||
|  | @ -119,6 +138,71 @@ std::string WmoHeader::product_designator() const | ||||||
|    return p->productDesignator_; |    return p->productDesignator_; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | std::chrono::sys_time<std::chrono::minutes> WmoHeader::GetDateTime( | ||||||
|  |    std::optional<std::chrono::system_clock::time_point> endTimeHint) | ||||||
|  | { | ||||||
|  |    std::chrono::sys_time<std::chrono::minutes> wmoDateTime {}; | ||||||
|  | 
 | ||||||
|  |    const auto absoluteDateTime = p->absoluteDateTime_; | ||||||
|  | 
 | ||||||
|  |    if (absoluteDateTime.has_value()) | ||||||
|  |    { | ||||||
|  |       wmoDateTime = absoluteDateTime.value(); | ||||||
|  |    } | ||||||
|  |    else if (endTimeHint.has_value()) | ||||||
|  |    { | ||||||
|  |       bool          dateTimeValid = false; | ||||||
|  |       unsigned int  dayOfMonth    = 0; | ||||||
|  |       unsigned long hour          = 0; | ||||||
|  |       unsigned long minute        = 0; | ||||||
|  | 
 | ||||||
|  |       dateTimeValid = p->ParseDateTime(dayOfMonth, hour, minute); | ||||||
|  | 
 | ||||||
|  |       if (dateTimeValid) | ||||||
|  |       { | ||||||
|  |          using namespace std::chrono; | ||||||
|  | 
 | ||||||
|  |          const auto           endDays = floor<days>(endTimeHint.value()); | ||||||
|  |          const year_month_day endDate {endDays}; | ||||||
|  | 
 | ||||||
|  |          // Combine end date year and month with WMO date time
 | ||||||
|  |          wmoDateTime = | ||||||
|  |             sys_days {endDate.year() / endDate.month() / day {dayOfMonth}} + | ||||||
|  |             hours {hour} + minutes {minute}; | ||||||
|  | 
 | ||||||
|  |          // If the begin date is after the end date, assume the start time
 | ||||||
|  |          // was the previous month (give a 1 day grace period for expiring
 | ||||||
|  |          // events in the past)
 | ||||||
|  |          // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers)
 | ||||||
|  |          if (wmoDateTime > endTimeHint.value() + 24h) | ||||||
|  |          { | ||||||
|  |             // If the current end month is January
 | ||||||
|  |             if (endDate.month() == January) | ||||||
|  |             { | ||||||
|  |                // The begin month must be December of last year
 | ||||||
|  |                wmoDateTime = | ||||||
|  |                   sys_days { | ||||||
|  |                      year {static_cast<int>((endDate.year() - 1y).count())} / | ||||||
|  |                      December / day {dayOfMonth}} + | ||||||
|  |                   hours {hour} + minutes {minute}; | ||||||
|  |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                // Back up one month
 | ||||||
|  |                wmoDateTime = | ||||||
|  |                   sys_days {endDate.year() / | ||||||
|  |                             month {static_cast<unsigned int>( | ||||||
|  |                                (endDate.month() - month {1}).count())} / | ||||||
|  |                             day {dayOfMonth}} + | ||||||
|  |                   hours {hour} + minutes {minute}; | ||||||
|  |             } | ||||||
|  |          } | ||||||
|  |       } | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    return wmoDateTime; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| bool WmoHeader::Parse(std::istream& is) | bool WmoHeader::Parse(std::istream& is) | ||||||
| { | { | ||||||
|    bool headerValid = true; |    bool headerValid = true; | ||||||
|  | @ -132,9 +216,21 @@ bool WmoHeader::Parse(std::istream& is) | ||||||
|    { |    { | ||||||
|       util::getline(is, sohLine); |       util::getline(is, sohLine); | ||||||
|       util::getline(is, sequenceLine); |       util::getline(is, sequenceLine); | ||||||
|  |       util::getline(is, wmoLine); | ||||||
|  |    } | ||||||
|  |    else | ||||||
|  |    { | ||||||
|  |       // The next line could be the WMO line or the sequence line
 | ||||||
|  |       util::getline(is, wmoLine); | ||||||
|  |       if (wmoLine.length() < kWmoHeaderMinLineLength_) | ||||||
|  |       { | ||||||
|  |          // This is likely the sequence line instead
 | ||||||
|  |          sequenceLine.swap(wmoLine); | ||||||
|  |          util::getline(is, wmoLine); | ||||||
|  |       } | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|    util::getline(is, wmoLine); |    auto awipsLinePos = is.tellg(); | ||||||
|    util::getline(is, awipsLine); |    util::getline(is, awipsLine); | ||||||
| 
 | 
 | ||||||
|    if (is.eof()) |    if (is.eof()) | ||||||
|  | @ -179,17 +275,18 @@ bool WmoHeader::Parse(std::istream& is) | ||||||
|          logger_->warn("Invalid number of WMO tokens"); |          logger_->warn("Invalid number of WMO tokens"); | ||||||
|          headerValid = false; |          headerValid = false; | ||||||
|       } |       } | ||||||
|       else if (wmoTokenList[0].size() != 6) |       else if (wmoTokenList[0].size() < kWmoIdentifierLengthMin_ || | ||||||
|  |                wmoTokenList[0].size() > kWmoIdentifierLengthMax_) | ||||||
|       { |       { | ||||||
|          logger_->warn("WMO identifier malformed"); |          logger_->warn("WMO identifier malformed"); | ||||||
|          headerValid = false; |          headerValid = false; | ||||||
|       } |       } | ||||||
|       else if (wmoTokenList[1].size() != 4) |       else if (wmoTokenList[1].size() != kIcaoLength_) | ||||||
|       { |       { | ||||||
|          logger_->warn("ICAO malformed"); |          logger_->warn("ICAO malformed"); | ||||||
|          headerValid = false; |          headerValid = false; | ||||||
|       } |       } | ||||||
|       else if (wmoTokenList[2].size() != 6) |       else if (wmoTokenList[2].size() != kDateTimeLength_) | ||||||
|       { |       { | ||||||
|          logger_->warn("Date/time malformed"); |          logger_->warn("Date/time malformed"); | ||||||
|          headerValid = false; |          headerValid = false; | ||||||
|  | @ -204,10 +301,12 @@ bool WmoHeader::Parse(std::istream& is) | ||||||
|       { |       { | ||||||
|          p->dataType_             = wmoTokenList[0].substr(0, 2); |          p->dataType_             = wmoTokenList[0].substr(0, 2); | ||||||
|          p->geographicDesignator_ = wmoTokenList[0].substr(2, 2); |          p->geographicDesignator_ = wmoTokenList[0].substr(2, 2); | ||||||
|          p->bulletinId_           = wmoTokenList[0].substr(4, 2); |          p->bulletinId_ = wmoTokenList[0].substr(4, wmoTokenList[0].size() - 4); | ||||||
|          p->icao_       = wmoTokenList[1]; |          p->icao_       = wmoTokenList[1]; | ||||||
|          p->dateTime_   = wmoTokenList[2]; |          p->dateTime_   = wmoTokenList[2]; | ||||||
| 
 | 
 | ||||||
|  |          p->CalculateAbsoluteDateTime(); | ||||||
|  | 
 | ||||||
|          if (wmoTokenList.size() == 4) |          if (wmoTokenList.size() == 4) | ||||||
|          { |          { | ||||||
|             p->bbbIndicator_ = wmoTokenList[3]; |             p->bbbIndicator_ = wmoTokenList[3]; | ||||||
|  | @ -224,10 +323,14 @@ bool WmoHeader::Parse(std::istream& is) | ||||||
| 
 | 
 | ||||||
|    if (headerValid) |    if (headerValid) | ||||||
|    { |    { | ||||||
|       if (awipsLine.size() != 6) |       if (awipsLine.size() != kAwipsIdentifierLineLength_) | ||||||
|       { |       { | ||||||
|          logger_->warn("AWIPS Identifier Line bad size"); |          // Older products may be missing an AWIPS Identifier Line
 | ||||||
|          headerValid = false; |          logger_->trace("AWIPS Identifier Line bad size"); | ||||||
|  | 
 | ||||||
|  |          is.seekg(awipsLinePos); | ||||||
|  |          p->productCategory_   = ""; | ||||||
|  |          p->productDesignator_ = ""; | ||||||
|       } |       } | ||||||
|       else |       else | ||||||
|       { |       { | ||||||
|  | @ -239,5 +342,60 @@ bool WmoHeader::Parse(std::istream& is) | ||||||
|    return headerValid; |    return headerValid; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| } // namespace awips
 | void WmoHeader::SetDateHint(std::chrono::year_month dateHint) | ||||||
| } // namespace scwx
 | { | ||||||
|  |    p->dateHint_ = dateHint; | ||||||
|  |    p->CalculateAbsoluteDateTime(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool WmoHeaderImpl::ParseDateTime(unsigned int&  dayOfMonth, | ||||||
|  |                                   unsigned long& hour, | ||||||
|  |                                   unsigned long& minute) | ||||||
|  | { | ||||||
|  |    bool dateTimeValid = false; | ||||||
|  | 
 | ||||||
|  |    try | ||||||
|  |    { | ||||||
|  |       // WMO date time is in the format DDHHMM
 | ||||||
|  |       dayOfMonth = | ||||||
|  |          static_cast<unsigned int>(std::stoul(dateTime_.substr(0, 2))); | ||||||
|  |       hour          = std::stoul(dateTime_.substr(2, 2)); | ||||||
|  |       minute        = std::stoul(dateTime_.substr(4, 2)); | ||||||
|  |       dateTimeValid = true; | ||||||
|  |    } | ||||||
|  |    catch (const std::exception&) | ||||||
|  |    { | ||||||
|  |       logger_->warn("Malformed WMO date/time: {}", dateTime_); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    return dateTimeValid; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void WmoHeaderImpl::CalculateAbsoluteDateTime() | ||||||
|  | { | ||||||
|  |    bool dateTimeValid = false; | ||||||
|  | 
 | ||||||
|  |    if (dateHint_.has_value() && !dateTime_.empty()) | ||||||
|  |    { | ||||||
|  |       unsigned int  dayOfMonth = 0; | ||||||
|  |       unsigned long hour       = 0; | ||||||
|  |       unsigned long minute     = 0; | ||||||
|  | 
 | ||||||
|  |       dateTimeValid = ParseDateTime(dayOfMonth, hour, minute); | ||||||
|  | 
 | ||||||
|  |       if (dateTimeValid) | ||||||
|  |       { | ||||||
|  |          using namespace std::chrono; | ||||||
|  |          absoluteDateTime_ = sys_days {dateHint_->year() / dateHint_->month() / | ||||||
|  |                                        day {dayOfMonth}} + | ||||||
|  |                              hours {hour} + minutes {minute}; | ||||||
|  |       } | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    if (!dateTimeValid) | ||||||
|  |    { | ||||||
|  |       absoluteDateTime_.reset(); | ||||||
|  |    } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } // namespace scwx::awips
 | ||||||
|  |  | ||||||
							
								
								
									
										185
									
								
								wxdata/source/scwx/provider/iem_api_provider.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								wxdata/source/scwx/provider/iem_api_provider.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,185 @@ | ||||||
|  | #include <scwx/provider/iem_api_provider.ipp> | ||||||
|  | #include <scwx/util/json.hpp> | ||||||
|  | #include <scwx/util/logger.hpp> | ||||||
|  | 
 | ||||||
|  | #include <boost/json.hpp> | ||||||
|  | #include <cpr/cpr.h> | ||||||
|  | #include <range/v3/iterator/operations.hpp> | ||||||
|  | #include <range/v3/range/conversion.hpp> | ||||||
|  | #include <range/v3/view/cartesian_product.hpp> | ||||||
|  | #include <range/v3/view/single.hpp> | ||||||
|  | 
 | ||||||
|  | #if (__cpp_lib_chrono < 201907L) | ||||||
|  | #   include <date/date.h> | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | namespace scwx::provider | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | static const std::string logPrefix_ = "scwx::provider::iem_api_provider"; | ||||||
|  | 
 | ||||||
|  | const std::shared_ptr<spdlog::logger> IemApiProvider::logger_ = | ||||||
|  |    util::Logger::Create(logPrefix_); | ||||||
|  | 
 | ||||||
|  | const std::string IemApiProvider::kBaseUrl_ = | ||||||
|  |    "https://mesonet.agron.iastate.edu/api/1"; | ||||||
|  | 
 | ||||||
|  | const std::string IemApiProvider::kListNwsTextProductsEndpoint_ = | ||||||
|  |    "/nws/afos/list.json"; | ||||||
|  | const std::string IemApiProvider::kNwsTextProductEndpoint_ = "/nwstext/"; | ||||||
|  | 
 | ||||||
|  | class IemApiProvider::Impl | ||||||
|  | { | ||||||
|  | public: | ||||||
|  |    explicit Impl()               = default; | ||||||
|  |    ~Impl()                       = default; | ||||||
|  |    Impl(const Impl&)             = delete; | ||||||
|  |    Impl& operator=(const Impl&)  = delete; | ||||||
|  |    Impl(const Impl&&)            = delete; | ||||||
|  |    Impl& operator=(const Impl&&) = delete; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | IemApiProvider::IemApiProvider() : p(std::make_unique<Impl>()) {} | ||||||
|  | IemApiProvider::~IemApiProvider() = default; | ||||||
|  | 
 | ||||||
|  | IemApiProvider::IemApiProvider(IemApiProvider&&) noexcept            = default; | ||||||
|  | IemApiProvider& IemApiProvider::operator=(IemApiProvider&&) noexcept = default; | ||||||
|  | 
 | ||||||
|  | boost::outcome_v2::result<std::vector<types::iem::AfosEntry>> | ||||||
|  | IemApiProvider::ListTextProducts(std::chrono::sys_days           date, | ||||||
|  |                                  std::optional<std::string_view> optionalCccc, | ||||||
|  |                                  std::optional<std::string_view> optionalPil) | ||||||
|  | { | ||||||
|  |    const std::string_view cccc = | ||||||
|  |       optionalCccc.has_value() ? optionalCccc.value() : std::string_view {}; | ||||||
|  |    const std::string_view pil = | ||||||
|  |       optionalPil.has_value() ? optionalPil.value() : std::string_view {}; | ||||||
|  | 
 | ||||||
|  |    const auto dateArray = std::array {date}; | ||||||
|  |    const auto ccccArray = std::array {cccc}; | ||||||
|  |    const auto pilArray  = std::array {pil}; | ||||||
|  | 
 | ||||||
|  |    return ListTextProducts(dateArray, ccccArray, pilArray); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | boost::outcome_v2::result<std::vector<types::iem::AfosEntry>> | ||||||
|  | IemApiProvider::ProcessTextProductLists( | ||||||
|  |    std::vector<cpr::AsyncResponse>& asyncResponses) | ||||||
|  | { | ||||||
|  |    std::vector<types::iem::AfosEntry> textProducts {}; | ||||||
|  | 
 | ||||||
|  |    for (auto& asyncResponse : asyncResponses) | ||||||
|  |    { | ||||||
|  |       auto response = asyncResponse.get(); | ||||||
|  | 
 | ||||||
|  |       const boost::json::value json = util::json::ReadJsonString(response.text); | ||||||
|  | 
 | ||||||
|  |       if (response.status_code == cpr::status::HTTP_OK) | ||||||
|  |       { | ||||||
|  |          try | ||||||
|  |          { | ||||||
|  |             // Get AFOS list from response
 | ||||||
|  |             auto entries = boost::json::value_to<types::iem::AfosList>(json); | ||||||
|  |             textProducts.insert(textProducts.end(), | ||||||
|  |                                 std::make_move_iterator(entries.data_.begin()), | ||||||
|  |                                 std::make_move_iterator(entries.data_.end())); | ||||||
|  |          } | ||||||
|  |          catch (const std::exception& ex) | ||||||
|  |          { | ||||||
|  |             // Unexpected bad response
 | ||||||
|  |             logger_->warn("Error parsing JSON: {}", ex.what()); | ||||||
|  |             return boost::system::errc::make_error_code( | ||||||
|  |                boost::system::errc::bad_message); | ||||||
|  |          } | ||||||
|  |       } | ||||||
|  |       else if (response.status_code == cpr::status::HTTP_BAD_REQUEST && | ||||||
|  |                json != nullptr) | ||||||
|  |       { | ||||||
|  |          try | ||||||
|  |          { | ||||||
|  |             // Log bad request details
 | ||||||
|  |             auto badRequest = | ||||||
|  |                boost::json::value_to<types::iem::BadRequest>(json); | ||||||
|  |             logger_->warn("ListTextProducts bad request: {}", | ||||||
|  |                           badRequest.detail_); | ||||||
|  |          } | ||||||
|  |          catch (const std::exception& ex) | ||||||
|  |          { | ||||||
|  |             // Unexpected bad response
 | ||||||
|  |             logger_->warn("Error parsing bad response: {}", ex.what()); | ||||||
|  |          } | ||||||
|  | 
 | ||||||
|  |          return boost::system::errc::make_error_code( | ||||||
|  |             boost::system::errc::invalid_argument); | ||||||
|  |       } | ||||||
|  |       else if (response.status_code == cpr::status::HTTP_UNPROCESSABLE_ENTITY && | ||||||
|  |                json != nullptr) | ||||||
|  |       { | ||||||
|  |          try | ||||||
|  |          { | ||||||
|  |             // Log validation error details
 | ||||||
|  |             auto error = | ||||||
|  |                boost::json::value_to<types::iem::ValidationError>(json); | ||||||
|  |             logger_->warn("ListTextProducts validation error: {}", | ||||||
|  |                           error.detail_.at(0).msg_); | ||||||
|  |          } | ||||||
|  |          catch (const std::exception& ex) | ||||||
|  |          { | ||||||
|  |             // Unexpected bad response
 | ||||||
|  |             logger_->warn("Error parsing validation error: {}", ex.what()); | ||||||
|  |          } | ||||||
|  | 
 | ||||||
|  |          return boost::system::errc::make_error_code( | ||||||
|  |             boost::system::errc::no_message_available); | ||||||
|  |       } | ||||||
|  |       else | ||||||
|  |       { | ||||||
|  |          logger_->warn("Could not list text products: {}", | ||||||
|  |                        response.status_line); | ||||||
|  | 
 | ||||||
|  |          return boost::system::errc::make_error_code( | ||||||
|  |             boost::system::errc::no_message); | ||||||
|  |       } | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    logger_->debug("Found {} products", textProducts.size()); | ||||||
|  | 
 | ||||||
|  |    return textProducts; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::vector<std::shared_ptr<awips::TextProductFile>> | ||||||
|  | IemApiProvider::ProcessTextProductFiles( | ||||||
|  |    std::vector<std::pair<std::string, cpr::AsyncResponse>>& asyncResponses) | ||||||
|  | { | ||||||
|  |    std::vector<std::shared_ptr<awips::TextProductFile>> textProductFiles; | ||||||
|  | 
 | ||||||
|  |    for (auto& asyncResponse : asyncResponses) | ||||||
|  |    { | ||||||
|  |       auto response = asyncResponse.second.get(); | ||||||
|  | 
 | ||||||
|  |       if (response.status_code == cpr::status::HTTP_OK) | ||||||
|  |       { | ||||||
|  |          // Load file
 | ||||||
|  |          auto& productId = asyncResponse.first; | ||||||
|  |          const std::shared_ptr<awips::TextProductFile> textProductFile { | ||||||
|  |             std::make_shared<awips::TextProductFile>()}; | ||||||
|  |          std::istringstream responseBody {response.text}; | ||||||
|  |          if (textProductFile->LoadData(productId, responseBody)) | ||||||
|  |          { | ||||||
|  |             textProductFiles.push_back(textProductFile); | ||||||
|  |          } | ||||||
|  |       } | ||||||
|  |       else | ||||||
|  |       { | ||||||
|  |          logger_->warn("Could not load text product: {} ({})", | ||||||
|  |                        asyncResponse.first, | ||||||
|  |                        response.status_line); | ||||||
|  |       } | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    logger_->debug("Loaded {} text products", textProductFiles.size()); | ||||||
|  | 
 | ||||||
|  |    return textProductFiles; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } // namespace scwx::provider
 | ||||||
|  | @ -1,9 +1,19 @@ | ||||||
| #include <scwx/provider/warnings_provider.hpp> | // Prevent redefinition of __cpp_lib_format
 | ||||||
| #include <scwx/network/dir_list.hpp> | #if defined(_MSC_VER) | ||||||
| #include <scwx/util/logger.hpp> | #   include <yvals_core.h> | ||||||
|  | #endif | ||||||
| 
 | 
 | ||||||
| #include <ranges> | // Enable chrono formatters
 | ||||||
| #include <shared_mutex> | #ifndef __cpp_lib_format | ||||||
|  | // NOLINTNEXTLINE(bugprone-reserved-identifier, cppcoreguidelines-macro-usage)
 | ||||||
|  | #   define __cpp_lib_format 202110L | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | #include <scwx/provider/warnings_provider.hpp> | ||||||
|  | #include <scwx/util/logger.hpp> | ||||||
|  | #include <scwx/util/time.hpp> | ||||||
|  | 
 | ||||||
|  | #include <mutex> | ||||||
| 
 | 
 | ||||||
| #if defined(_MSC_VER) | #if defined(_MSC_VER) | ||||||
| #   pragma warning(push, 0) | #   pragma warning(push, 0) | ||||||
|  | @ -11,8 +21,6 @@ | ||||||
| 
 | 
 | ||||||
| #define LIBXML_HTML_ENABLED | #define LIBXML_HTML_ENABLED | ||||||
| #include <cpr/cpr.h> | #include <cpr/cpr.h> | ||||||
| #include <libxml/HTMLparser.h> |  | ||||||
| #include <re2/re2.h> |  | ||||||
| 
 | 
 | ||||||
| #if (__cpp_lib_chrono < 201907L) | #if (__cpp_lib_chrono < 201907L) | ||||||
| #   include <date/date.h> | #   include <date/date.h> | ||||||
|  | @ -22,9 +30,7 @@ | ||||||
| #   pragma warning(pop) | #   pragma warning(pop) | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx::provider | ||||||
| { |  | ||||||
| namespace provider |  | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| static const std::string logPrefix_ = "scwx::provider::warnings_provider"; | static const std::string logPrefix_ = "scwx::provider::warnings_provider"; | ||||||
|  | @ -35,25 +41,36 @@ class WarningsProvider::Impl | ||||||
| public: | public: | ||||||
|    struct FileInfoRecord |    struct FileInfoRecord | ||||||
|    { |    { | ||||||
|       std::chrono::system_clock::time_point startTime_ {}; |       FileInfoRecord(std::string contentLength, std::string lastModified) : | ||||||
|       std::chrono::system_clock::time_point lastModified_ {}; |           contentLengthStr_ {std::move(contentLength)}, | ||||||
|       size_t                                size_ {}; |           lastModifiedStr_ {std::move(lastModified)} | ||||||
|       bool                                  updated_ {}; |  | ||||||
|    }; |  | ||||||
| 
 |  | ||||||
|    typedef std::map<std::string, FileInfoRecord> WarningFileMap; |  | ||||||
| 
 |  | ||||||
|    explicit Impl(const std::string& baseUrl) : |  | ||||||
|        baseUrl_ {baseUrl}, files_ {}, filesMutex_ {} |  | ||||||
|       { |       { | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|    ~Impl() {} |       std::string contentLengthStr_ {}; | ||||||
|  |       std::string lastModifiedStr_ {}; | ||||||
|  |    }; | ||||||
|  | 
 | ||||||
|  |    using WarningFileMap = std::map<std::string, FileInfoRecord>; | ||||||
|  | 
 | ||||||
|  |    explicit Impl(std::string baseUrl) : | ||||||
|  |        baseUrl_ {std::move(baseUrl)}, files_ {}, filesMutex_ {} | ||||||
|  |    { | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    ~Impl()                       = default; | ||||||
|  |    Impl(const Impl&)             = delete; | ||||||
|  |    Impl& operator=(const Impl&)  = delete; | ||||||
|  |    Impl(const Impl&&)            = delete; | ||||||
|  |    Impl& operator=(const Impl&&) = delete; | ||||||
|  | 
 | ||||||
|  |    bool UpdateFileRecord(const cpr::Response& response, | ||||||
|  |                          const std::string&   filename); | ||||||
| 
 | 
 | ||||||
|    std::string baseUrl_; |    std::string baseUrl_; | ||||||
| 
 | 
 | ||||||
|    WarningFileMap files_; |    WarningFileMap files_; | ||||||
|    std::shared_mutex filesMutex_; |    std::mutex     filesMutex_; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| WarningsProvider::WarningsProvider(const std::string& baseUrl) : | WarningsProvider::WarningsProvider(const std::string& baseUrl) : | ||||||
|  | @ -66,145 +83,177 @@ WarningsProvider::WarningsProvider(WarningsProvider&&) noexcept = default; | ||||||
| WarningsProvider& | WarningsProvider& | ||||||
| WarningsProvider::operator=(WarningsProvider&&) noexcept = default; | WarningsProvider::operator=(WarningsProvider&&) noexcept = default; | ||||||
| 
 | 
 | ||||||
| std::pair<size_t, size_t> | std::vector<std::shared_ptr<awips::TextProductFile>> | ||||||
| WarningsProvider::ListFiles(std::chrono::system_clock::time_point newerThan) | WarningsProvider::LoadUpdatedFiles( | ||||||
|  |    std::chrono::sys_time<std::chrono::hours> startTime) | ||||||
| { | { | ||||||
|    using namespace std::chrono; |    using namespace std::chrono; | ||||||
| 
 | 
 | ||||||
| #if (__cpp_lib_chrono < 201907L) | #if (__cpp_lib_chrono >= 201907L) | ||||||
|  |    namespace df = std; | ||||||
|  | 
 | ||||||
|  |    static constexpr std::string_view kDateTimeFormat { | ||||||
|  |       "warnings_{:%Y%m%d_%H}.txt"}; | ||||||
|  | #else | ||||||
|    using namespace date; |    using namespace date; | ||||||
|  |    namespace df = date; | ||||||
|  | 
 | ||||||
|  | #   define kDateTimeFormat "warnings_%Y%m%d_%H.txt" | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|    static constexpr LazyRE2 reWarningsFilename = { |    std::vector< | ||||||
|       "warnings_[0-9]{8}_[0-9]{2}.txt"}; |       std::pair<std::string, | ||||||
|    static const std::string dateTimeFormat {"warnings_%Y%m%d_%H.txt"}; |                 cpr::AsyncWrapper<std::optional<cpr::AsyncResponse>, false>>> | ||||||
| 
 |                                                         asyncCallbacks; | ||||||
|    logger_->trace("Listing files"); |  | ||||||
| 
 |  | ||||||
|    size_t updatedObjects = 0; |  | ||||||
|    size_t totalObjects   = 0; |  | ||||||
| 
 |  | ||||||
|    // Perform a directory listing
 |  | ||||||
|    auto records = network::DirList(p->baseUrl_); |  | ||||||
| 
 |  | ||||||
|    // Sort records by filename
 |  | ||||||
|    std::sort(records.begin(), |  | ||||||
|              records.end(), |  | ||||||
|              [](auto& a, auto& b) { return a.filename_ < b.filename_; }); |  | ||||||
| 
 |  | ||||||
|    // Filter warning records
 |  | ||||||
|    auto warningRecords = |  | ||||||
|       records | |  | ||||||
|       std::views::filter( |  | ||||||
|          [](auto& record) |  | ||||||
|          { |  | ||||||
|             return record.type_ == std::filesystem::file_type::regular && |  | ||||||
|                    RE2::FullMatch(record.filename_, *reWarningsFilename); |  | ||||||
|          }); |  | ||||||
| 
 |  | ||||||
|    std::unique_lock lock(p->filesMutex_); |  | ||||||
| 
 |  | ||||||
|    Impl::WarningFileMap warningFileMap; |  | ||||||
| 
 |  | ||||||
|    // Store records
 |  | ||||||
|    for (auto& record : warningRecords) |  | ||||||
|    { |  | ||||||
|       // Determine start time
 |  | ||||||
|       std::chrono::sys_time<hours> startTime; |  | ||||||
|       std::istringstream           ssFilename {record.filename_}; |  | ||||||
| 
 |  | ||||||
|       ssFilename >> parse(dateTimeFormat, startTime); |  | ||||||
| 
 |  | ||||||
|       // If start time is valid
 |  | ||||||
|       if (!ssFilename.fail()) |  | ||||||
|       { |  | ||||||
|          // Determine if the record should be marked updated
 |  | ||||||
|          bool updated = true; |  | ||||||
|          auto it      = p->files_.find(record.filename_); |  | ||||||
|          if (it != p->files_.cend()) |  | ||||||
|          { |  | ||||||
|             auto& existingRecord = it->second; |  | ||||||
| 
 |  | ||||||
|             updated = existingRecord.updated_ || |  | ||||||
|                       record.size_ != existingRecord.size_ || |  | ||||||
|                       record.mtime_ != existingRecord.lastModified_; |  | ||||||
|          } |  | ||||||
| 
 |  | ||||||
|          // Update object counts, but only if newer than threshold
 |  | ||||||
|          if (newerThan < startTime) |  | ||||||
|          { |  | ||||||
|             if (updated) |  | ||||||
|             { |  | ||||||
|                ++updatedObjects; |  | ||||||
|             } |  | ||||||
|             ++totalObjects; |  | ||||||
|          } |  | ||||||
| 
 |  | ||||||
|          // Store record
 |  | ||||||
|          warningFileMap.emplace( |  | ||||||
|             std::piecewise_construct, |  | ||||||
|             std::forward_as_tuple(record.filename_), |  | ||||||
|             std::forward_as_tuple( |  | ||||||
|                startTime, record.mtime_, record.size_, updated)); |  | ||||||
|       } |  | ||||||
|    } |  | ||||||
| 
 |  | ||||||
|    p->files_ = std::move(warningFileMap); |  | ||||||
| 
 |  | ||||||
|    return std::make_pair(updatedObjects, totalObjects); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| std::vector<std::shared_ptr<awips::TextProductFile>> |  | ||||||
| WarningsProvider::LoadUpdatedFiles( |  | ||||||
|    std::chrono::system_clock::time_point newerThan) |  | ||||||
| { |  | ||||||
|    logger_->debug("Loading updated files"); |  | ||||||
| 
 |  | ||||||
|    std::vector<std::shared_ptr<awips::TextProductFile>> updatedFiles; |    std::vector<std::shared_ptr<awips::TextProductFile>> updatedFiles; | ||||||
| 
 | 
 | ||||||
|    std::vector<std::pair<std::string, cpr::AsyncResponse>> asyncResponses; |    const std::chrono::sys_time<std::chrono::hours> now = | ||||||
|  |       std::chrono::floor<std::chrono::hours>(std::chrono::system_clock::now()); | ||||||
|  |    std::chrono::sys_time<std::chrono::hours> currentHour = | ||||||
|  |       (startTime != std::chrono::sys_time<std::chrono::hours> {}) ? | ||||||
|  |          startTime : | ||||||
|  |          now - std::chrono::hours {1}; | ||||||
| 
 | 
 | ||||||
|    std::unique_lock lock(p->filesMutex_); |    logger_->trace("Querying files newer than: {}", util::TimeString(startTime)); | ||||||
| 
 | 
 | ||||||
|    // For each warning file
 |    while (currentHour <= now) | ||||||
|    for (auto& record : p->files_) |  | ||||||
|    { |    { | ||||||
|       // If file is updated, and time is later than the threshold
 |       const std::string filename = df::format(kDateTimeFormat, currentHour); | ||||||
|       if (record.second.updated_ && newerThan < record.second.startTime_) |       const std::string url      = p->baseUrl_ + "/" + filename; | ||||||
|       { |  | ||||||
|          // Retrieve warning file
 |  | ||||||
|          asyncResponses.emplace_back( |  | ||||||
|             record.first, |  | ||||||
|             cpr::GetAsync(cpr::Url {p->baseUrl_ + "/" + record.first})); |  | ||||||
| 
 | 
 | ||||||
|          // Clear updated flag
 |       logger_->trace("HEAD request for file: {}", filename); | ||||||
|          record.second.updated_ = false; | 
 | ||||||
|  |       asyncCallbacks.emplace_back( | ||||||
|  |          filename, | ||||||
|  |          cpr::HeadCallback( | ||||||
|  |             [url, filename, this]( | ||||||
|  |                cpr::Response headResponse) -> std::optional<cpr::AsyncResponse> | ||||||
|  |             { | ||||||
|  |                if (headResponse.status_code == cpr::status::HTTP_OK) | ||||||
|  |                { | ||||||
|  |                   const bool updated = | ||||||
|  |                      p->UpdateFileRecord(headResponse, filename); | ||||||
|  | 
 | ||||||
|  |                   if (updated) | ||||||
|  |                   { | ||||||
|  |                      logger_->trace("GET request for file: {}", filename); | ||||||
|  |                      return cpr::GetAsync(cpr::Url {url}); | ||||||
|                   } |                   } | ||||||
|                } |                } | ||||||
| 
 |                else if (headResponse.status_code != cpr::status::HTTP_NOT_FOUND) | ||||||
|    lock.unlock(); |  | ||||||
| 
 |  | ||||||
|    // Wait for warning files to load
 |  | ||||||
|    for (auto& asyncResponse : asyncResponses) |  | ||||||
|                { |                { | ||||||
|       cpr::Response response = asyncResponse.second.get(); |                   logger_->warn("HEAD request for file failed: {} ({})", | ||||||
|  |                                 url, | ||||||
|  |                                 headResponse.status_line); | ||||||
|  |                } | ||||||
|  | 
 | ||||||
|  |                return std::nullopt; | ||||||
|  |             }, | ||||||
|  |             cpr::Url {url})); | ||||||
|  | 
 | ||||||
|  |       // Query the next hour
 | ||||||
|  |       currentHour += 1h; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    for (auto& asyncCallback : asyncCallbacks) | ||||||
|  |    { | ||||||
|  |       auto& filename = asyncCallback.first; | ||||||
|  |       auto& callback = asyncCallback.second; | ||||||
|  | 
 | ||||||
|  |       if (callback.valid()) | ||||||
|  |       { | ||||||
|  |          // Wait for futures to complete
 | ||||||
|  |          callback.wait(); | ||||||
|  |          auto asyncResponse = callback.get(); | ||||||
|  | 
 | ||||||
|  |          if (asyncResponse.has_value()) | ||||||
|  |          { | ||||||
|  |             auto response = asyncResponse.value().get(); | ||||||
|  | 
 | ||||||
|             if (response.status_code == cpr::status::HTTP_OK) |             if (response.status_code == cpr::status::HTTP_OK) | ||||||
|             { |             { | ||||||
|          logger_->debug("Loading file: {}", asyncResponse.first); |                logger_->debug("Loading file: {}", filename); | ||||||
| 
 | 
 | ||||||
|                // Load file
 |                // Load file
 | ||||||
|          std::shared_ptr<awips::TextProductFile> textProductFile { |                const std::shared_ptr<awips::TextProductFile> textProductFile { | ||||||
|                   std::make_shared<awips::TextProductFile>()}; |                   std::make_shared<awips::TextProductFile>()}; | ||||||
|                std::istringstream responseBody {response.text}; |                std::istringstream responseBody {response.text}; | ||||||
|          if (textProductFile->LoadData(responseBody)) |                if (textProductFile->LoadData(filename, responseBody)) | ||||||
|                { |                { | ||||||
|                   updatedFiles.push_back(textProductFile); |                   updatedFiles.push_back(textProductFile); | ||||||
|                } |                } | ||||||
|             } |             } | ||||||
|  |             else | ||||||
|  |             { | ||||||
|  |                logger_->warn("Could not load file: {} ({})", | ||||||
|  |                              filename, | ||||||
|  |                              response.status_line); | ||||||
|  |             } | ||||||
|  |          } | ||||||
|  |       } | ||||||
|  |       else | ||||||
|  |       { | ||||||
|  |          logger_->error("Invalid future state"); | ||||||
|  |       } | ||||||
|    } |    } | ||||||
| 
 | 
 | ||||||
|    return updatedFiles; |    return updatedFiles; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| } // namespace provider
 | bool WarningsProvider::Impl::UpdateFileRecord(const cpr::Response& response, | ||||||
| } // namespace scwx
 |                                               const std::string&   filename) | ||||||
|  | { | ||||||
|  |    bool updated = false; | ||||||
|  | 
 | ||||||
|  |    auto contentLengthIt = response.header.find("Content-Length"); | ||||||
|  |    auto lastModifiedIt  = response.header.find("Last-Modified"); | ||||||
|  | 
 | ||||||
|  |    std::string contentLength {}; | ||||||
|  |    std::string lastModified {}; | ||||||
|  | 
 | ||||||
|  |    if (contentLengthIt != response.header.cend()) | ||||||
|  |    { | ||||||
|  |       contentLength = contentLengthIt->second; | ||||||
|  |    } | ||||||
|  |    if (lastModifiedIt != response.header.cend()) | ||||||
|  |    { | ||||||
|  |       lastModified = lastModifiedIt->second; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    const std::unique_lock lock(filesMutex_); | ||||||
|  | 
 | ||||||
|  |    auto it = files_.find(filename); | ||||||
|  |    if (it != files_.cend()) | ||||||
|  |    { | ||||||
|  |       auto& existingRecord = it->second; | ||||||
|  | 
 | ||||||
|  |       // If the size or last modified changes, request an update
 | ||||||
|  | 
 | ||||||
|  |       if (!contentLength.empty() && | ||||||
|  |           contentLength != existingRecord.contentLengthStr_) | ||||||
|  |       { | ||||||
|  |          // Size changed
 | ||||||
|  |          existingRecord.contentLengthStr_ = contentLengthIt->second; | ||||||
|  |          updated                          = true; | ||||||
|  |       } | ||||||
|  |       else if (!lastModified.empty() && | ||||||
|  |                lastModified != existingRecord.lastModifiedStr_) | ||||||
|  |       { | ||||||
|  |          // Last modified changed
 | ||||||
|  |          existingRecord.lastModifiedStr_ = lastModifiedIt->second; | ||||||
|  |          updated                         = true; | ||||||
|  |       } | ||||||
|  |    } | ||||||
|  |    else | ||||||
|  |    { | ||||||
|  |       // File not found
 | ||||||
|  |       files_.emplace(std::piecewise_construct, | ||||||
|  |                      std::forward_as_tuple(filename), | ||||||
|  |                      std::forward_as_tuple(contentLength, lastModified)); | ||||||
|  |       updated = true; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    return updated; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } // namespace scwx::provider
 | ||||||
|  |  | ||||||
							
								
								
									
										111
									
								
								wxdata/source/scwx/types/iem_types.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								wxdata/source/scwx/types/iem_types.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,111 @@ | ||||||
|  | #include <scwx/types/iem_types.hpp> | ||||||
|  | 
 | ||||||
|  | #include <boost/json/value_to.hpp> | ||||||
|  | 
 | ||||||
|  | namespace scwx::types::iem | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | AfosEntry tag_invoke(boost::json::value_to_tag<AfosEntry>, | ||||||
|  |                      const boost::json::value& jv) | ||||||
|  | { | ||||||
|  |    auto& jo = jv.as_object(); | ||||||
|  | 
 | ||||||
|  |    AfosEntry entry {}; | ||||||
|  | 
 | ||||||
|  |    // Required parameters
 | ||||||
|  |    entry.index_     = jo.at("index").as_int64(); | ||||||
|  |    entry.entered_   = jo.at("entered").as_string(); | ||||||
|  |    entry.pil_       = jo.at("pil").as_string(); | ||||||
|  |    entry.productId_ = jo.at("product_id").as_string(); | ||||||
|  |    entry.cccc_      = jo.at("cccc").as_string(); | ||||||
|  |    entry.count_     = jo.at("count").as_int64(); | ||||||
|  |    entry.link_      = jo.at("link").as_string(); | ||||||
|  |    entry.textLink_  = jo.at("text_link").as_string(); | ||||||
|  | 
 | ||||||
|  |    return entry; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | AfosList tag_invoke(boost::json::value_to_tag<AfosList>, | ||||||
|  |                     const boost::json::value& jv) | ||||||
|  | { | ||||||
|  |    auto& jo = jv.as_object(); | ||||||
|  | 
 | ||||||
|  |    AfosList list {}; | ||||||
|  | 
 | ||||||
|  |    // Required parameters
 | ||||||
|  |    list.data_ = boost::json::value_to<std::vector<AfosEntry>>(jo.at("data")); | ||||||
|  | 
 | ||||||
|  |    return list; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | BadRequest tag_invoke(boost::json::value_to_tag<BadRequest>, | ||||||
|  |                       const boost::json::value& jv) | ||||||
|  | { | ||||||
|  |    auto& jo = jv.as_object(); | ||||||
|  | 
 | ||||||
|  |    BadRequest badRequest {}; | ||||||
|  | 
 | ||||||
|  |    // Required parameters
 | ||||||
|  |    badRequest.detail_ = jo.at("detail").as_string(); | ||||||
|  | 
 | ||||||
|  |    return badRequest; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ValidationError::Detail::Context | ||||||
|  | tag_invoke(boost::json::value_to_tag<ValidationError::Detail::Context>, | ||||||
|  |            const boost::json::value& jv) | ||||||
|  | { | ||||||
|  |    auto& jo = jv.as_object(); | ||||||
|  | 
 | ||||||
|  |    ValidationError::Detail::Context ctx {}; | ||||||
|  | 
 | ||||||
|  |    // Required parameters
 | ||||||
|  |    ctx.error_ = jo.at("error").as_string(); | ||||||
|  | 
 | ||||||
|  |    return ctx; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ValidationError::Detail | ||||||
|  | tag_invoke(boost::json::value_to_tag<ValidationError::Detail>, | ||||||
|  |            const boost::json::value& jv) | ||||||
|  | { | ||||||
|  |    auto& jo = jv.as_object(); | ||||||
|  | 
 | ||||||
|  |    ValidationError::Detail detail {}; | ||||||
|  | 
 | ||||||
|  |    // Required parameters
 | ||||||
|  |    detail.type_ = jo.at("type").as_string(); | ||||||
|  |    detail.loc_  = boost::json::value_to< | ||||||
|  |        std::vector<std::variant<std::int64_t, std::string>>>(jo.at("loc")); | ||||||
|  |    detail.msg_ = jo.at("msg").as_string(); | ||||||
|  | 
 | ||||||
|  |    // Optional parameters
 | ||||||
|  |    if (jo.contains("input")) | ||||||
|  |    { | ||||||
|  |       detail.input_ = jo.at("input").as_string(); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    if (jo.contains("ctx")) | ||||||
|  |    { | ||||||
|  |       detail.ctx_ = | ||||||
|  |          boost::json::value_to<ValidationError::Detail::Context>(jo.at("ctx")); | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    return detail; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ValidationError tag_invoke(boost::json::value_to_tag<ValidationError>, | ||||||
|  |                            const boost::json::value& jv) | ||||||
|  | { | ||||||
|  |    auto& jo = jv.as_object(); | ||||||
|  | 
 | ||||||
|  |    ValidationError error {}; | ||||||
|  | 
 | ||||||
|  |    // Required parameters
 | ||||||
|  |    error.detail_ = boost::json::value_to<std::vector<ValidationError::Detail>>( | ||||||
|  |       jo.at("detail")); | ||||||
|  | 
 | ||||||
|  |    return error; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } // namespace scwx::types::iem
 | ||||||
							
								
								
									
										201
									
								
								wxdata/source/scwx/util/json.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								wxdata/source/scwx/util/json.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,201 @@ | ||||||
|  | #include <scwx/util/json.hpp> | ||||||
|  | #include <scwx/util/logger.hpp> | ||||||
|  | 
 | ||||||
|  | #include <fstream> | ||||||
|  | 
 | ||||||
|  | #include <boost/json.hpp> | ||||||
|  | #include <fmt/ranges.h> | ||||||
|  | 
 | ||||||
|  | namespace scwx::util::json | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | static const std::string logPrefix_ = "scwx::util::json"; | ||||||
|  | static const auto        logger_    = scwx::util::Logger::Create(logPrefix_); | ||||||
|  | 
 | ||||||
|  | /* Adapted from:
 | ||||||
|  |  * https://www.boost.org/doc/libs/1_77_0/libs/json/doc/html/json/examples.html#json.examples.pretty
 | ||||||
|  |  * | ||||||
|  |  * Copyright (c) 2019, 2020 Vinnie Falco | ||||||
|  |  * Copyright (c) 2020 Krystian Stasiowski | ||||||
|  |  * Distributed under the Boost Software License, Version 1.0. (See | ||||||
|  |  * http://www.boost.org/LICENSE_1_0.txt)
 | ||||||
|  |  */ | ||||||
|  | static void PrettyPrintJson(std::ostream&             os, | ||||||
|  |                             boost::json::value const& jv, | ||||||
|  |                             std::string*              indent = nullptr); | ||||||
|  | 
 | ||||||
|  | boost::json::value ReadJsonFile(const std::string& path) | ||||||
|  | { | ||||||
|  |    boost::json::value json; | ||||||
|  | 
 | ||||||
|  |    std::ifstream ifs {path}; | ||||||
|  |    json = ReadJsonStream(ifs); | ||||||
|  | 
 | ||||||
|  |    return json; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | boost::json::value ReadJsonStream(std::istream& is) | ||||||
|  | { | ||||||
|  |    std::string line; | ||||||
|  | 
 | ||||||
|  |    boost::json::stream_parser p; | ||||||
|  |    boost::system::error_code  ec; | ||||||
|  | 
 | ||||||
|  |    while (std::getline(is, line)) | ||||||
|  |    { | ||||||
|  |       p.write(line, ec); | ||||||
|  |       if (ec) | ||||||
|  |       { | ||||||
|  |          logger_->warn("{}", ec.message()); | ||||||
|  |          return nullptr; | ||||||
|  |       } | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    p.finish(ec); | ||||||
|  |    if (ec) | ||||||
|  |    { | ||||||
|  |       logger_->warn("{}", ec.message()); | ||||||
|  |       return nullptr; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    return p.release(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | boost::json::value ReadJsonString(std::string_view sv) | ||||||
|  | { | ||||||
|  |    boost::json::stream_parser p; | ||||||
|  |    boost::system::error_code  ec; | ||||||
|  | 
 | ||||||
|  |    p.write(sv, ec); | ||||||
|  |    if (ec) | ||||||
|  |    { | ||||||
|  |       logger_->warn("{}", ec.message()); | ||||||
|  |       return nullptr; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    p.finish(ec); | ||||||
|  |    if (ec) | ||||||
|  |    { | ||||||
|  |       logger_->warn("{}", ec.message()); | ||||||
|  |       return nullptr; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    return p.release(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void WriteJsonFile(const std::string&        path, | ||||||
|  |                    const boost::json::value& json, | ||||||
|  |                    bool                      prettyPrint) | ||||||
|  | { | ||||||
|  |    std::ofstream ofs {path}; | ||||||
|  | 
 | ||||||
|  |    if (!ofs.is_open()) | ||||||
|  |    { | ||||||
|  |       logger_->warn("Cannot write JSON file: \"{}\"", path); | ||||||
|  |    } | ||||||
|  |    else | ||||||
|  |    { | ||||||
|  |       if (prettyPrint) | ||||||
|  |       { | ||||||
|  |          PrettyPrintJson(ofs, json); | ||||||
|  |       } | ||||||
|  |       else | ||||||
|  |       { | ||||||
|  |          ofs << json; | ||||||
|  |       } | ||||||
|  |       ofs.close(); | ||||||
|  |    } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Allow recursion within the pretty print function
 | ||||||
|  | // NOLINTNEXTLINE(misc-no-recursion)
 | ||||||
|  | static void PrettyPrintJson(std::ostream&             os, | ||||||
|  |                             boost::json::value const& jv, | ||||||
|  |                             std::string*              indent) | ||||||
|  | { | ||||||
|  |    std::string indent_; | ||||||
|  |    if (!indent) | ||||||
|  |       indent = &indent_; | ||||||
|  |    switch (jv.kind()) | ||||||
|  |    { | ||||||
|  |    case boost::json::kind::object: | ||||||
|  |    { | ||||||
|  |       os << "{\n"; | ||||||
|  |       indent->append(4, ' '); | ||||||
|  |       auto const& obj = jv.get_object(); | ||||||
|  |       if (!obj.empty()) | ||||||
|  |       { | ||||||
|  |          auto it = obj.begin(); | ||||||
|  |          for (;;) | ||||||
|  |          { | ||||||
|  |             os << *indent << boost::json::serialize(it->key()) << " : "; | ||||||
|  |             PrettyPrintJson(os, it->value(), indent); | ||||||
|  |             if (++it == obj.end()) | ||||||
|  |                break; | ||||||
|  |             os << ",\n"; | ||||||
|  |          } | ||||||
|  |       } | ||||||
|  |       os << "\n"; | ||||||
|  |       indent->resize(indent->size() - 4); | ||||||
|  |       os << *indent << "}"; | ||||||
|  |       break; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    case boost::json::kind::array: | ||||||
|  |    { | ||||||
|  |       os << "[\n"; | ||||||
|  |       indent->append(4, ' '); | ||||||
|  |       auto const& arr = jv.get_array(); | ||||||
|  |       if (!arr.empty()) | ||||||
|  |       { | ||||||
|  |          auto it = arr.begin(); | ||||||
|  |          for (;;) | ||||||
|  |          { | ||||||
|  |             os << *indent; | ||||||
|  |             PrettyPrintJson(os, *it, indent); | ||||||
|  |             if (++it == arr.end()) | ||||||
|  |                break; | ||||||
|  |             os << ",\n"; | ||||||
|  |          } | ||||||
|  |       } | ||||||
|  |       os << "\n"; | ||||||
|  |       indent->resize(indent->size() - 4); | ||||||
|  |       os << *indent << "]"; | ||||||
|  |       break; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    case boost::json::kind::string: | ||||||
|  |    { | ||||||
|  |       os << boost::json::serialize(jv.get_string()); | ||||||
|  |       break; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    case boost::json::kind::uint64: | ||||||
|  |       os << jv.get_uint64(); | ||||||
|  |       break; | ||||||
|  | 
 | ||||||
|  |    case boost::json::kind::int64: | ||||||
|  |       os << jv.get_int64(); | ||||||
|  |       break; | ||||||
|  | 
 | ||||||
|  |    case boost::json::kind::double_: | ||||||
|  |       os << jv.get_double(); | ||||||
|  |       break; | ||||||
|  | 
 | ||||||
|  |    case boost::json::kind::bool_: | ||||||
|  |       if (jv.get_bool()) | ||||||
|  |          os << "true"; | ||||||
|  |       else | ||||||
|  |          os << "false"; | ||||||
|  |       break; | ||||||
|  | 
 | ||||||
|  |    case boost::json::kind::null: | ||||||
|  |       os << "null"; | ||||||
|  |       break; | ||||||
|  |    } | ||||||
|  | 
 | ||||||
|  |    if (indent->empty()) | ||||||
|  |       os << "\n"; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } // namespace scwx::util::json
 | ||||||
|  | @ -1,8 +1,7 @@ | ||||||
| #include <scwx/util/streams.hpp> | #include <scwx/util/streams.hpp> | ||||||
|  | #include <scwx/common/characters.hpp> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx::util | ||||||
| { |  | ||||||
| namespace util |  | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| std::istream& getline(std::istream& is, std::string& t) | std::istream& getline(std::istream& is, std::string& t) | ||||||
|  | @ -17,7 +16,8 @@ std::istream& getline(std::istream& is, std::string& t) | ||||||
|       int c = sb->sbumpc(); |       int c = sb->sbumpc(); | ||||||
|       switch (c) |       switch (c) | ||||||
|       { |       { | ||||||
|       case '\n': return is; |       case '\n': | ||||||
|  |          return is; | ||||||
| 
 | 
 | ||||||
|       case '\r': |       case '\r': | ||||||
|          while (sb->sgetc() == '\r') |          while (sb->sgetc() == '\r') | ||||||
|  | @ -30,6 +30,10 @@ std::istream& getline(std::istream& is, std::string& t) | ||||||
|          } |          } | ||||||
|          return is; |          return is; | ||||||
| 
 | 
 | ||||||
|  |       case common::Characters::ETX: | ||||||
|  |          sb->sungetc(); | ||||||
|  |          return is; | ||||||
|  | 
 | ||||||
|       case std::streambuf::traits_type::eof(): |       case std::streambuf::traits_type::eof(): | ||||||
|          if (t.empty()) |          if (t.empty()) | ||||||
|          { |          { | ||||||
|  | @ -37,10 +41,10 @@ std::istream& getline(std::istream& is, std::string& t) | ||||||
|          } |          } | ||||||
|          return is; |          return is; | ||||||
| 
 | 
 | ||||||
|       default: t += static_cast<char>(c); |       default: | ||||||
|  |          t += static_cast<char>(c); | ||||||
|       } |       } | ||||||
|    } |    } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| } // namespace util
 | } // namespace scwx::util
 | ||||||
| } // namespace scwx
 |  | ||||||
|  |  | ||||||
|  | @ -21,9 +21,7 @@ | ||||||
| #   include <date/date.h> | #   include <date/date.h> | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx::util | ||||||
| { |  | ||||||
| namespace util |  | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| static const std::string logPrefix_ = "scwx::util::time"; | static const std::string logPrefix_ = "scwx::util::time"; | ||||||
|  | @ -48,6 +46,7 @@ std::chrono::system_clock::time_point TimePoint(uint32_t modifiedJulianDate, | ||||||
|    using sys_days       = time_point<system_clock, days>; |    using sys_days       = time_point<system_clock, days>; | ||||||
|    constexpr auto epoch = sys_days {1969y / December / 31d}; |    constexpr auto epoch = sys_days {1969y / December / 31d}; | ||||||
| 
 | 
 | ||||||
|  |    // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers): literals are used
 | ||||||
|    return epoch + (modifiedJulianDate * 24h) + |    return epoch + (modifiedJulianDate * 24h) + | ||||||
|           std::chrono::milliseconds {milliseconds}; |           std::chrono::milliseconds {milliseconds}; | ||||||
| } | } | ||||||
|  | @ -60,15 +59,19 @@ std::string TimeString(std::chrono::system_clock::time_point time, | ||||||
|    using namespace std::chrono; |    using namespace std::chrono; | ||||||
| 
 | 
 | ||||||
| #if (__cpp_lib_chrono >= 201907L) | #if (__cpp_lib_chrono >= 201907L) | ||||||
| #   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 date = std::chrono; | ||||||
|    namespace df   = std; |    namespace df   = std; | ||||||
|  | 
 | ||||||
|  |    static constexpr std::string_view kFormatString24Hour = | ||||||
|  |       "{:%Y-%m-%d %H:%M:%S %Z}"; | ||||||
|  |    static constexpr std::string_view kFormatString12Hour = | ||||||
|  |       "{:%Y-%m-%d %I:%M:%S %p %Z}"; | ||||||
| #else | #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; |    using namespace date; | ||||||
|    namespace df = date; |    namespace df = date; | ||||||
|  | 
 | ||||||
|  | #   define kFormatString24Hour "%Y-%m-%d %H:%M:%S %Z" | ||||||
|  | #   define kFormatString12Hour "%Y-%m-%d %I:%M:%S %p %Z" | ||||||
| #endif | #endif | ||||||
| 
 | 
 | ||||||
|    auto               timeInSeconds = time_point_cast<seconds>(time); |    auto               timeInSeconds = time_point_cast<seconds>(time); | ||||||
|  | @ -84,11 +87,11 @@ std::string TimeString(std::chrono::system_clock::time_point time, | ||||||
| 
 | 
 | ||||||
|             if (clockFormat == ClockFormat::_24Hour) |             if (clockFormat == ClockFormat::_24Hour) | ||||||
|             { |             { | ||||||
|                os << df::format(FORMAT_STRING_24_HOUR, zt); |                os << df::format(kFormatString24Hour, zt); | ||||||
|             } |             } | ||||||
|             else |             else | ||||||
|             { |             { | ||||||
|                os << df::format(FORMAT_STRING_12_HOUR, zt); |                os << df::format(kFormatString12Hour, zt); | ||||||
|             } |             } | ||||||
|          } |          } | ||||||
|          catch (const std::exception& ex) |          catch (const std::exception& ex) | ||||||
|  | @ -110,11 +113,11 @@ std::string TimeString(std::chrono::system_clock::time_point time, | ||||||
|       { |       { | ||||||
|          if (clockFormat == ClockFormat::_24Hour) |          if (clockFormat == ClockFormat::_24Hour) | ||||||
|          { |          { | ||||||
|             os << df::format(FORMAT_STRING_24_HOUR, timeInSeconds); |             os << df::format(kFormatString24Hour, timeInSeconds); | ||||||
|          } |          } | ||||||
|          else |          else | ||||||
|          { |          { | ||||||
|             os << df::format(FORMAT_STRING_12_HOUR, timeInSeconds); |             os << df::format(kFormatString12Hour, timeInSeconds); | ||||||
|          } |          } | ||||||
|       } |       } | ||||||
|    } |    } | ||||||
|  | @ -150,5 +153,4 @@ template std::optional<std::chrono::sys_time<std::chrono::seconds>> | ||||||
| TryParseDateTime<std::chrono::seconds>(const std::string& dateTimeFormat, | TryParseDateTime<std::chrono::seconds>(const std::string& dateTimeFormat, | ||||||
|                                        const std::string& str); |                                        const std::string& str); | ||||||
| 
 | 
 | ||||||
| } // namespace util
 | } // namespace scwx::util
 | ||||||
| } // namespace scwx
 |  | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ find_package(Boost) | ||||||
| find_package(cpr) | find_package(cpr) | ||||||
| find_package(LibXml2) | find_package(LibXml2) | ||||||
| find_package(OpenSSL) | find_package(OpenSSL) | ||||||
|  | find_package(range-v3) | ||||||
| find_package(re2) | find_package(re2) | ||||||
| find_package(spdlog) | find_package(spdlog) | ||||||
| 
 | 
 | ||||||
|  | @ -61,21 +62,27 @@ set(SRC_NETWORK source/scwx/network/cpr.cpp | ||||||
| set(HDR_PROVIDER include/scwx/provider/aws_level2_data_provider.hpp | set(HDR_PROVIDER include/scwx/provider/aws_level2_data_provider.hpp | ||||||
|                  include/scwx/provider/aws_level3_data_provider.hpp |                  include/scwx/provider/aws_level3_data_provider.hpp | ||||||
|                  include/scwx/provider/aws_nexrad_data_provider.hpp |                  include/scwx/provider/aws_nexrad_data_provider.hpp | ||||||
|  |                  include/scwx/provider/iem_api_provider.hpp | ||||||
|  |                  include/scwx/provider/iem_api_provider.ipp | ||||||
|                  include/scwx/provider/nexrad_data_provider.hpp |                  include/scwx/provider/nexrad_data_provider.hpp | ||||||
|                  include/scwx/provider/nexrad_data_provider_factory.hpp |                  include/scwx/provider/nexrad_data_provider_factory.hpp | ||||||
|                  include/scwx/provider/warnings_provider.hpp) |                  include/scwx/provider/warnings_provider.hpp) | ||||||
| set(SRC_PROVIDER source/scwx/provider/aws_level2_data_provider.cpp | set(SRC_PROVIDER source/scwx/provider/aws_level2_data_provider.cpp | ||||||
|                  source/scwx/provider/aws_level3_data_provider.cpp |                  source/scwx/provider/aws_level3_data_provider.cpp | ||||||
|                  source/scwx/provider/aws_nexrad_data_provider.cpp |                  source/scwx/provider/aws_nexrad_data_provider.cpp | ||||||
|  |                  source/scwx/provider/iem_api_provider.cpp | ||||||
|                  source/scwx/provider/nexrad_data_provider.cpp |                  source/scwx/provider/nexrad_data_provider.cpp | ||||||
|                  source/scwx/provider/nexrad_data_provider_factory.cpp |                  source/scwx/provider/nexrad_data_provider_factory.cpp | ||||||
|                  source/scwx/provider/warnings_provider.cpp) |                  source/scwx/provider/warnings_provider.cpp) | ||||||
|  | set(HDR_TYPES include/scwx/types/iem_types.hpp) | ||||||
|  | set(SRC_TYPES source/scwx/types/iem_types.cpp) | ||||||
| set(HDR_UTIL include/scwx/util/digest.hpp | set(HDR_UTIL include/scwx/util/digest.hpp | ||||||
|              include/scwx/util/enum.hpp |              include/scwx/util/enum.hpp | ||||||
|              include/scwx/util/environment.hpp |              include/scwx/util/environment.hpp | ||||||
|              include/scwx/util/float.hpp |              include/scwx/util/float.hpp | ||||||
|              include/scwx/util/hash.hpp |              include/scwx/util/hash.hpp | ||||||
|              include/scwx/util/iterator.hpp |              include/scwx/util/iterator.hpp | ||||||
|  |              include/scwx/util/json.hpp | ||||||
|              include/scwx/util/logger.hpp |              include/scwx/util/logger.hpp | ||||||
|              include/scwx/util/map.hpp |              include/scwx/util/map.hpp | ||||||
|              include/scwx/util/rangebuf.hpp |              include/scwx/util/rangebuf.hpp | ||||||
|  | @ -88,6 +95,7 @@ set(SRC_UTIL source/scwx/util/digest.cpp | ||||||
|              source/scwx/util/environment.cpp |              source/scwx/util/environment.cpp | ||||||
|              source/scwx/util/float.cpp |              source/scwx/util/float.cpp | ||||||
|              source/scwx/util/hash.cpp |              source/scwx/util/hash.cpp | ||||||
|  |              source/scwx/util/json.cpp | ||||||
|              source/scwx/util/logger.cpp |              source/scwx/util/logger.cpp | ||||||
|              source/scwx/util/rangebuf.cpp |              source/scwx/util/rangebuf.cpp | ||||||
|              source/scwx/util/streams.cpp |              source/scwx/util/streams.cpp | ||||||
|  | @ -224,6 +232,8 @@ add_library(wxdata OBJECT ${HDR_AWIPS} | ||||||
|                           ${SRC_NETWORK} |                           ${SRC_NETWORK} | ||||||
|                           ${HDR_PROVIDER} |                           ${HDR_PROVIDER} | ||||||
|                           ${SRC_PROVIDER} |                           ${SRC_PROVIDER} | ||||||
|  |                           ${HDR_TYPES} | ||||||
|  |                           ${SRC_TYPES} | ||||||
|                           ${HDR_UTIL} |                           ${HDR_UTIL} | ||||||
|                           ${SRC_UTIL} |                           ${SRC_UTIL} | ||||||
|                           ${HDR_WSR88D} |                           ${HDR_WSR88D} | ||||||
|  | @ -244,6 +254,8 @@ source_group("Header Files\\network"     FILES ${HDR_NETWORK}) | ||||||
| source_group("Source Files\\network"     FILES ${SRC_NETWORK}) | source_group("Source Files\\network"     FILES ${SRC_NETWORK}) | ||||||
| source_group("Header Files\\provider"    FILES ${HDR_PROVIDER}) | source_group("Header Files\\provider"    FILES ${HDR_PROVIDER}) | ||||||
| source_group("Source Files\\provider"    FILES ${SRC_PROVIDER}) | source_group("Source Files\\provider"    FILES ${SRC_PROVIDER}) | ||||||
|  | source_group("Header Files\\types"       FILES ${HDR_TYPES}) | ||||||
|  | source_group("Source Files\\types"       FILES ${SRC_TYPES}) | ||||||
| source_group("Header Files\\util"        FILES ${HDR_UTIL}) | source_group("Header Files\\util"        FILES ${HDR_UTIL}) | ||||||
| source_group("Source Files\\util"        FILES ${SRC_UTIL}) | source_group("Source Files\\util"        FILES ${SRC_UTIL}) | ||||||
| source_group("Header Files\\wsr88d"      FILES ${HDR_WSR88D}) | source_group("Header Files\\wsr88d"      FILES ${HDR_WSR88D}) | ||||||
|  | @ -293,6 +305,7 @@ target_link_libraries(wxdata PUBLIC aws-cpp-sdk-core | ||||||
|                                     cpr::cpr |                                     cpr::cpr | ||||||
|                                     LibXml2::LibXml2 |                                     LibXml2::LibXml2 | ||||||
|                                     OpenSSL::Crypto |                                     OpenSSL::Crypto | ||||||
|  |                                     range-v3::range-v3 | ||||||
|                                     re2::re2 |                                     re2::re2 | ||||||
|                                     spdlog::spdlog |                                     spdlog::spdlog | ||||||
|                                     units::units) |                                     units::units) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Dan Paulat
						Dan Paulat