From 6ebcd762e98d461212310a90655073a466be3a81 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 26 Nov 2023 07:38:37 -0600 Subject: [PATCH 01/29] Add EAS attention signal audio --- ACKNOWLEDGEMENTS.md | 1 + ...rgency_Alert_System_Attention_Signal_20s.ogg | Bin 0 -> 69451 bytes scwx-qt/scwx-qt.qrc | 1 + 3 files changed, 2 insertions(+) create mode 100644 scwx-qt/res/audio/wikimedia/Emergency_Alert_System_Attention_Signal_20s.ogg diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 4e6fffa0..8b08f4ee 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -59,6 +59,7 @@ Supercell Wx uses assets from the following sources: | Source | License | Notes | | ------ | ------- | ----- | | Alte DIN 1451 Mittelschrift | SIL Open Font License | +| [EAS Attention Signal](https://en.wikipedia.org/wiki/File:Emergency_Alert_System_Attention_Signal_20s.ogg) | Public Domain | | [Font Awesome Free](https://fontawesome.com/) | CC BY 4.0 License | | [Inconsolata](https://fonts.google.com/specimen/Inconsolata) | SIL Open Font License | | [NOAA's Weather and Climate Toolkit](https://www.ncdc.noaa.gov/wct/) | Public Domain | Default Color Tables | diff --git a/scwx-qt/res/audio/wikimedia/Emergency_Alert_System_Attention_Signal_20s.ogg b/scwx-qt/res/audio/wikimedia/Emergency_Alert_System_Attention_Signal_20s.ogg new file mode 100644 index 0000000000000000000000000000000000000000..d3c86e3cdb0fc85424232aa3a438f5158db11afb GIT binary patch literal 69451 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 literal 0 HcmV?d00001 diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index 3e7e55dd..51cc16df 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -12,6 +12,7 @@ gl/texture2d.frag gl/texture2d_array.frag gl/threshold.geom + res/audio/wikimedia/Emergency_Alert_System_Attention_Signal_20s.ogg res/config/radar_sites.json res/fonts/din1451alt.ttf res/fonts/din1451alt_g.ttf From 9830caebeb8ff4a73f705b78c0c22c146d254356 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 26 Nov 2023 22:19:16 -0600 Subject: [PATCH 02/29] Add Qt Multimedia dependency for playing audio --- .github/workflows/ci.yml | 4 ++-- scwx-qt/scwx-qt.cmake | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79b20b12..d9213f31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: msvc_version: 2022 qt_version: 6.6.1 qt_arch: win64_msvc2019_64 - qt_modules: qtimageformats qtpositioning + qt_modules: qtimageformats qtmultimedia qtpositioning qt_tools: '' conan_arch: x86_64 conan_compiler: Visual Studio @@ -46,7 +46,7 @@ jobs: compiler: gcc qt_version: 6.6.1 qt_arch: gcc_64 - qt_modules: qtimageformats qtpositioning + qt_modules: qtimageformats qtmultimedia qtpositioning qt_tools: '' conan_arch: x86_64 conan_compiler: gcc diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 809306e9..c026000b 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -22,6 +22,7 @@ find_package(SQLite3) find_package(QT NAMES Qt6 COMPONENTS Gui LinguistTools + Multimedia Network OpenGL OpenGLWidgets @@ -31,6 +32,7 @@ find_package(QT NAMES Qt6 find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Gui LinguistTools + Multimedia Network OpenGL OpenGLWidgets @@ -512,6 +514,7 @@ endif() target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::OpenGLWidgets + Qt${QT_VERSION_MAJOR}::Multimedia Qt${QT_VERSION_MAJOR}::Positioning Boost::json Boost::timer From a89c20c697e740d47ea34e4d786b395ce195920a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 26 Nov 2023 22:31:14 -0600 Subject: [PATCH 03/29] Media Manager stub --- scwx-qt/scwx-qt.cmake | 2 + .../source/scwx/qt/manager/media_manager.cpp | 45 +++++++++++++++++++ .../source/scwx/qt/manager/media_manager.hpp | 32 +++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 scwx-qt/source/scwx/qt/manager/media_manager.cpp create mode 100644 scwx-qt/source/scwx/qt/manager/media_manager.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index c026000b..8b15dfe5 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -79,6 +79,7 @@ set(SRC_GL_DRAW source/scwx/qt/gl/draw/draw_item.cpp source/scwx/qt/gl/draw/placefile_triangles.cpp source/scwx/qt/gl/draw/rectangle.cpp) set(HDR_MANAGER source/scwx/qt/manager/font_manager.hpp + source/scwx/qt/manager/media_manager.hpp source/scwx/qt/manager/placefile_manager.hpp source/scwx/qt/manager/position_manager.hpp source/scwx/qt/manager/radar_product_manager.hpp @@ -89,6 +90,7 @@ set(HDR_MANAGER source/scwx/qt/manager/font_manager.hpp source/scwx/qt/manager/timeline_manager.hpp source/scwx/qt/manager/update_manager.hpp) set(SRC_MANAGER source/scwx/qt/manager/font_manager.cpp + source/scwx/qt/manager/media_manager.cpp source/scwx/qt/manager/placefile_manager.cpp source/scwx/qt/manager/position_manager.cpp source/scwx/qt/manager/radar_product_manager.cpp diff --git a/scwx-qt/source/scwx/qt/manager/media_manager.cpp b/scwx-qt/source/scwx/qt/manager/media_manager.cpp new file mode 100644 index 00000000..23896e63 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/media_manager.cpp @@ -0,0 +1,45 @@ +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +static const std::string logPrefix_ = "scwx::qt::manager::media_manager"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class MediaManager::Impl +{ +public: + explicit Impl() {} + + ~Impl() {} +}; + +MediaManager::MediaManager() : p(std::make_unique()) {} +MediaManager::~MediaManager() = default; + +std::shared_ptr MediaManager::Instance() +{ + static std::weak_ptr mediaManagerReference_ {}; + static std::mutex instanceMutex_ {}; + + std::unique_lock lock(instanceMutex_); + + std::shared_ptr mediaManager = mediaManagerReference_.lock(); + + if (mediaManager == nullptr) + { + mediaManager = std::make_shared(); + mediaManagerReference_ = mediaManager; + } + + return mediaManager; +} + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/media_manager.hpp b/scwx-qt/source/scwx/qt/manager/media_manager.hpp new file mode 100644 index 00000000..958bfc82 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/media_manager.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +class MediaManager : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(MediaManager) + +public: + explicit MediaManager(); + ~MediaManager(); + + static std::shared_ptr Instance(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace manager +} // namespace qt +} // namespace scwx From 318f35aebd4c68c82ce4211dfca810ee8f07363e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 27 Nov 2023 05:49:02 -0600 Subject: [PATCH 04/29] Add media types for pre-defined audio files --- scwx-qt/scwx-qt.cmake | 2 ++ scwx-qt/source/scwx/qt/types/media_types.cpp | 24 +++++++++++++++++ scwx-qt/source/scwx/qt/types/media_types.hpp | 27 ++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 scwx-qt/source/scwx/qt/types/media_types.cpp create mode 100644 scwx-qt/source/scwx/qt/types/media_types.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 8b15dfe5..85cf8ee4 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -173,6 +173,7 @@ set(HDR_TYPES source/scwx/qt/types/alert_types.hpp source/scwx/qt/types/imgui_font.hpp source/scwx/qt/types/layer_types.hpp source/scwx/qt/types/map_types.hpp + source/scwx/qt/types/media_types.hpp source/scwx/qt/types/qt_types.hpp source/scwx/qt/types/radar_product_record.hpp source/scwx/qt/types/text_event_key.hpp @@ -183,6 +184,7 @@ set(SRC_TYPES source/scwx/qt/types/alert_types.cpp source/scwx/qt/types/imgui_font.cpp source/scwx/qt/types/layer_types.cpp source/scwx/qt/types/map_types.cpp + source/scwx/qt/types/media_types.cpp source/scwx/qt/types/qt_types.cpp source/scwx/qt/types/radar_product_record.cpp source/scwx/qt/types/text_event_key.cpp diff --git a/scwx-qt/source/scwx/qt/types/media_types.cpp b/scwx-qt/source/scwx/qt/types/media_types.cpp new file mode 100644 index 00000000..a8279e73 --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/media_types.cpp @@ -0,0 +1,24 @@ +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +static const std::unordered_map audioFileInfo_ { + {AudioFile::EasAttentionSignal, + "qrc:/res/audio/wikimedia/" + "Emergency_Alert_System_Attention_Signal_20s.ogg"}}; + +const std::string& GetMediaPath(AudioFile audioFile) +{ + return audioFileInfo_.at(audioFile); +} + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/media_types.hpp b/scwx-qt/source/scwx/qt/types/media_types.hpp new file mode 100644 index 00000000..641edd2c --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/media_types.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +enum class AudioFile +{ + EasAttentionSignal +}; +typedef scwx::util::Iterator + AudioFileIterator; + +const std::string& GetMediaPath(AudioFile audioFile); + +} // namespace types +} // namespace qt +} // namespace scwx From 3ad3c98daf6ce86a39ea899b3bb8adbde9336b23 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 27 Nov 2023 05:53:41 -0600 Subject: [PATCH 05/29] Add play audio functionality to media manager --- .../source/scwx/qt/manager/media_manager.cpp | 62 ++++++++++++++++++- .../source/scwx/qt/manager/media_manager.hpp | 4 ++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/media_manager.cpp b/scwx-qt/source/scwx/qt/manager/media_manager.cpp index 23896e63..d5ad895b 100644 --- a/scwx-qt/source/scwx/qt/manager/media_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/media_manager.cpp @@ -1,6 +1,11 @@ #include #include +#include +#include +#include +#include + namespace scwx { namespace qt @@ -14,14 +19,67 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); class MediaManager::Impl { public: - explicit Impl() {} + explicit Impl(MediaManager* self) : + self_ {self}, + mediaPlayer_ {new QMediaPlayer(self)}, + audioOutput_ {new QAudioOutput(self)} + { + audioOutput_->setVolume(1.0f); + mediaPlayer_->setAudioOutput(audioOutput_); + + logger_->debug("Audio device: {}", + audioOutput_->device().description().toStdString()); + + ConnectSignals(); + } ~Impl() {} + + void ConnectSignals(); + + MediaManager* self_; + + QMediaPlayer* mediaPlayer_; + QAudioOutput* audioOutput_; }; -MediaManager::MediaManager() : p(std::make_unique()) {} +MediaManager::MediaManager() : p(std::make_unique(this)) {} MediaManager::~MediaManager() = default; +void MediaManager::Impl::ConnectSignals() +{ + QObject::connect(audioOutput_, + &QAudioOutput::deviceChanged, + self_, + [this]() + { + logger_->debug( + "Audio device changed: {}", + audioOutput_->device().description().toStdString()); + }); + + QObject::connect(mediaPlayer_, + &QMediaPlayer::errorOccurred, + self_, + [](QMediaPlayer::Error error, const QString& errorString) + { + logger_->error("Error {}: {}", + static_cast(error), + errorString.toStdString()); + }); +} + +void MediaManager::Play(types::AudioFile media) +{ + const std::string path = types::GetMediaPath(media); + + logger_->debug("Playing audio: {}", path); + + p->mediaPlayer_->setSource(QUrl(QString::fromStdString(path))); + + QMetaObject::invokeMethod(p->mediaPlayer_, &QMediaPlayer::play); +} + std::shared_ptr MediaManager::Instance() { static std::weak_ptr mediaManagerReference_ {}; diff --git a/scwx-qt/source/scwx/qt/manager/media_manager.hpp b/scwx-qt/source/scwx/qt/manager/media_manager.hpp index 958bfc82..1b6f7ce9 100644 --- a/scwx-qt/source/scwx/qt/manager/media_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/media_manager.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -20,6 +22,8 @@ class MediaManager : public QObject explicit MediaManager(); ~MediaManager(); + void Play(types::AudioFile media); + static std::shared_ptr Instance(); private: From e1ccc1ebb8bdeea13c19865948dc6cf2ab2f8fb9 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Mon, 27 Nov 2023 06:09:43 -0600 Subject: [PATCH 06/29] Audio should follow default device --- .../source/scwx/qt/manager/media_manager.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/media_manager.cpp b/scwx-qt/source/scwx/qt/manager/media_manager.cpp index d5ad895b..53caba8c 100644 --- a/scwx-qt/source/scwx/qt/manager/media_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/media_manager.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -21,15 +22,15 @@ class MediaManager::Impl public: explicit Impl(MediaManager* self) : self_ {self}, + mediaDevices_ {new QMediaDevices(self)}, mediaPlayer_ {new QMediaPlayer(self)}, audioOutput_ {new QAudioOutput(self)} { - audioOutput_->setVolume(1.0f); - mediaPlayer_->setAudioOutput(audioOutput_); - logger_->debug("Audio device: {}", audioOutput_->device().description().toStdString()); + mediaPlayer_->setAudioOutput(audioOutput_); + ConnectSignals(); } @@ -39,8 +40,9 @@ class MediaManager::Impl MediaManager* self_; - QMediaPlayer* mediaPlayer_; - QAudioOutput* audioOutput_; + QMediaDevices* mediaDevices_; + QMediaPlayer* mediaPlayer_; + QAudioOutput* audioOutput_; }; MediaManager::MediaManager() : p(std::make_unique(this)) {} @@ -48,6 +50,13 @@ MediaManager::~MediaManager() = default; void MediaManager::Impl::ConnectSignals() { + QObject::connect( + mediaDevices_, + &QMediaDevices::audioOutputsChanged, + self_, + [this]() + { audioOutput_->setDevice(QMediaDevices::defaultAudioOutput()); }); + QObject::connect(audioOutput_, &QAudioOutput::deviceChanged, self_, From 9486d2364a1f28772aca2b1c05812b76015a396e Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 29 Nov 2023 06:00:30 -0600 Subject: [PATCH 07/29] Add types for audio alerts --- scwx-qt/scwx-qt.cmake | 2 ++ scwx-qt/source/scwx/qt/types/alert_types.cpp | 11 +++++++ scwx-qt/source/scwx/qt/types/alert_types.hpp | 4 +++ .../source/scwx/qt/types/location_types.cpp | 29 +++++++++++++++++++ .../source/scwx/qt/types/location_types.hpp | 29 +++++++++++++++++++ wxdata/include/scwx/util/enum.hpp | 20 +++++++++++++ wxdata/wxdata.cmake | 3 +- 7 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 scwx-qt/source/scwx/qt/types/location_types.cpp create mode 100644 scwx-qt/source/scwx/qt/types/location_types.hpp create mode 100644 wxdata/include/scwx/util/enum.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 85cf8ee4..116a5355 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -172,6 +172,7 @@ set(HDR_TYPES source/scwx/qt/types/alert_types.hpp source/scwx/qt/types/github_types.hpp source/scwx/qt/types/imgui_font.hpp source/scwx/qt/types/layer_types.hpp + source/scwx/qt/types/location_types.hpp source/scwx/qt/types/map_types.hpp source/scwx/qt/types/media_types.hpp source/scwx/qt/types/qt_types.hpp @@ -183,6 +184,7 @@ set(SRC_TYPES source/scwx/qt/types/alert_types.cpp source/scwx/qt/types/github_types.cpp source/scwx/qt/types/imgui_font.cpp source/scwx/qt/types/layer_types.cpp + source/scwx/qt/types/location_types.cpp source/scwx/qt/types/map_types.cpp source/scwx/qt/types/media_types.cpp source/scwx/qt/types/qt_types.cpp diff --git a/scwx-qt/source/scwx/qt/types/alert_types.cpp b/scwx-qt/source/scwx/qt/types/alert_types.cpp index 34e44a95..46b1e785 100644 --- a/scwx-qt/source/scwx/qt/types/alert_types.cpp +++ b/scwx-qt/source/scwx/qt/types/alert_types.cpp @@ -37,6 +37,17 @@ std::string GetAlertActionName(AlertAction alertAction) return alertActionName_.at(alertAction); } +const std::vector& GetAlertAudioPhenomena() +{ + static const std::vector phenomena_ { + awips::Phenomenon::FlashFlood, + awips::Phenomenon::SevereThunderstorm, + awips::Phenomenon::SnowSquall, + awips::Phenomenon::Tornado}; + + return phenomena_; +} + } // namespace types } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/alert_types.hpp b/scwx-qt/source/scwx/qt/types/alert_types.hpp index 19431d4c..489b6b7a 100644 --- a/scwx-qt/source/scwx/qt/types/alert_types.hpp +++ b/scwx-qt/source/scwx/qt/types/alert_types.hpp @@ -1,8 +1,10 @@ #pragma once +#include #include #include +#include namespace scwx { @@ -23,6 +25,8 @@ typedef scwx::util::Iterator AlertAction GetAlertAction(const std::string& name); std::string GetAlertActionName(AlertAction alertAction); +const std::vector& GetAlertAudioPhenomena(); + } // namespace types } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/location_types.cpp b/scwx-qt/source/scwx/qt/types/location_types.cpp new file mode 100644 index 00000000..619e7746 --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/location_types.cpp @@ -0,0 +1,29 @@ +#include +#include + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +static const std::unordered_map + locationMethodName_ {{LocationMethod::Fixed, "Fixed"}, + {LocationMethod::Track, "Track"}, + {LocationMethod::Unknown, "?"}}; + +SCWX_GET_ENUM(LocationMethod, GetLocationMethod, locationMethodName_) + +const std::string& GetLocationMethodName(LocationMethod locationMethod) +{ + return locationMethodName_.at(locationMethod); +} + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/location_types.hpp b/scwx-qt/source/scwx/qt/types/location_types.hpp new file mode 100644 index 00000000..9a6cca05 --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/location_types.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +enum class LocationMethod +{ + Fixed, + Track, + Unknown +}; +typedef scwx::util:: + Iterator + LocationMethodIterator; + +LocationMethod GetLocationMethod(const std::string& name); +const std::string& GetLocationMethodName(LocationMethod locationMethod); + +} // namespace types +} // namespace qt +} // namespace scwx diff --git a/wxdata/include/scwx/util/enum.hpp b/wxdata/include/scwx/util/enum.hpp new file mode 100644 index 00000000..7b991050 --- /dev/null +++ b/wxdata/include/scwx/util/enum.hpp @@ -0,0 +1,20 @@ +#pragma once + +#define SCWX_GET_ENUM(Type, FunctionName, nameMap) \ + Type FunctionName(const std::string& name) \ + { \ + auto result = \ + std::find_if(nameMap.cbegin(), \ + nameMap.cend(), \ + [&](const std::pair& pair) -> bool \ + { return boost::iequals(pair.second, name); }); \ + \ + if (result != nameMap.cend()) \ + { \ + return result->first; \ + } \ + else \ + { \ + return Type::Unknown; \ + } \ + } diff --git a/wxdata/wxdata.cmake b/wxdata/wxdata.cmake index f0507326..f074d858 100644 --- a/wxdata/wxdata.cmake +++ b/wxdata/wxdata.cmake @@ -66,7 +66,8 @@ set(SRC_PROVIDER source/scwx/provider/aws_level2_data_provider.cpp source/scwx/provider/nexrad_data_provider.cpp source/scwx/provider/nexrad_data_provider_factory.cpp source/scwx/provider/warnings_provider.cpp) -set(HDR_UTIL include/scwx/util/environment.hpp +set(HDR_UTIL include/scwx/util/enum.hpp + include/scwx/util/environment.hpp include/scwx/util/float.hpp include/scwx/util/hash.hpp include/scwx/util/iterator.hpp From ec97231bca5b7fb598f8d4b3ab144875222dd4d7 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 29 Nov 2023 06:05:35 -0600 Subject: [PATCH 08/29] Add audio settings --- scwx-qt/scwx-qt.cmake | 7 +- .../scwx/qt/manager/settings_manager.cpp | 4 + .../scwx/qt/settings/audio_settings.cpp | 131 ++++++++++++++++++ .../scwx/qt/settings/audio_settings.hpp | 45 ++++++ .../scwx/qt/settings/settings_definitions.hpp | 20 +++ test/data | 2 +- 6 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/settings/audio_settings.cpp create mode 100644 scwx-qt/source/scwx/qt/settings/audio_settings.hpp create mode 100644 scwx-qt/source/scwx/qt/settings/settings_definitions.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 116a5355..b9c14701 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -145,18 +145,21 @@ set(SRC_MODEL source/scwx/qt/model/alert_model.cpp source/scwx/qt/model/tree_model.cpp) set(HDR_REQUEST source/scwx/qt/request/nexrad_file_request.hpp) set(SRC_REQUEST source/scwx/qt/request/nexrad_file_request.cpp) -set(HDR_SETTINGS source/scwx/qt/settings/general_settings.hpp +set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp + source/scwx/qt/settings/general_settings.hpp source/scwx/qt/settings/map_settings.hpp source/scwx/qt/settings/palette_settings.hpp source/scwx/qt/settings/settings_category.hpp source/scwx/qt/settings/settings_container.hpp + source/scwx/qt/settings/settings_definitions.hpp source/scwx/qt/settings/settings_interface.hpp source/scwx/qt/settings/settings_interface_base.hpp source/scwx/qt/settings/settings_variable.hpp source/scwx/qt/settings/settings_variable_base.hpp source/scwx/qt/settings/text_settings.hpp source/scwx/qt/settings/ui_settings.hpp) -set(SRC_SETTINGS source/scwx/qt/settings/general_settings.cpp +set(SRC_SETTINGS source/scwx/qt/settings/audio_settings.cpp + source/scwx/qt/settings/general_settings.cpp source/scwx/qt/settings/map_settings.cpp source/scwx/qt/settings/palette_settings.cpp source/scwx/qt/settings/settings_category.cpp diff --git a/scwx-qt/source/scwx/qt/manager/settings_manager.cpp b/scwx-qt/source/scwx/qt/manager/settings_manager.cpp index a4c0f4a3..36ad057c 100644 --- a/scwx-qt/source/scwx/qt/manager/settings_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/settings_manager.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -128,6 +129,7 @@ boost::json::value SettingsManager::Impl::ConvertSettingsToJson() boost::json::object settingsJson; settings::GeneralSettings::Instance().WriteJson(settingsJson); + settings::AudioSettings::Instance().WriteJson(settingsJson); settings::MapSettings::Instance().WriteJson(settingsJson); settings::PaletteSettings::Instance().WriteJson(settingsJson); settings::TextSettings::Instance().WriteJson(settingsJson); @@ -141,6 +143,7 @@ void SettingsManager::Impl::GenerateDefaultSettings() logger_->info("Generating default settings"); settings::GeneralSettings::Instance().SetDefaults(); + settings::AudioSettings::Instance().SetDefaults(); settings::MapSettings::Instance().SetDefaults(); settings::PaletteSettings::Instance().SetDefaults(); settings::TextSettings::Instance().SetDefaults(); @@ -155,6 +158,7 @@ bool SettingsManager::Impl::LoadSettings( bool jsonDirty = false; jsonDirty |= !settings::GeneralSettings::Instance().ReadJson(settingsJson); + jsonDirty |= !settings::AudioSettings::Instance().ReadJson(settingsJson); jsonDirty |= !settings::MapSettings::Instance().ReadJson(settingsJson); jsonDirty |= !settings::PaletteSettings::Instance().ReadJson(settingsJson); jsonDirty |= !settings::TextSettings::Instance().ReadJson(settingsJson); diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp new file mode 100644 index 00000000..869973ae --- /dev/null +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp @@ -0,0 +1,131 @@ +#include +#include +#include +#include +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace settings +{ + +static const std::string logPrefix_ = "scwx::qt::settings::audio_settings"; + +static const bool kDefaultAlertEnabled_ {false}; +static const awips::Phenomenon kDefaultPhenomenon_ { + awips::Phenomenon::FlashFlood}; + +class AudioSettings::Impl +{ +public: + explicit Impl() + { + std::string defaultAlertLocationMethodValue = + types::GetLocationMethodName(types::LocationMethod::Fixed); + + boost::to_lower(defaultAlertLocationMethodValue); + + alertLocationMethod_.SetDefault(defaultAlertLocationMethodValue); + alertLatitude_.SetDefault(0.0); + alertLongitude_.SetDefault(0.0); + + alertLatitude_.SetMinimum(-90.0); + alertLatitude_.SetMaximum(90.0); + alertLongitude_.SetMinimum(-180.0); + alertLongitude_.SetMaximum(180.0); + + alertLocationMethod_.SetValidator( + SCWX_SETTINGS_ENUM_VALIDATOR(types::LocationMethod, + types::LocationMethodIterator(), + types::GetLocationMethodName)); + + for (auto& phenomenon : types::GetAlertAudioPhenomena()) + { + std::string phenomenonCode = awips::GetPhenomenonCode(phenomenon); + std::string name = fmt::format("{}_enabled", phenomenonCode); + + auto result = + alertEnabled_.emplace(phenomenon, SettingsVariable {name}); + + SettingsVariable& variable = result.first->second; + + variable.SetDefault(kDefaultAlertEnabled_); + + variables_.push_back(&variable); + } + } + + ~Impl() {} + + SettingsVariable alertLocationMethod_ {"alert_location_method"}; + SettingsVariable alertLatitude_ {"alert_latitude"}; + SettingsVariable alertLongitude_ {"alert_longitude"}; + + std::unordered_map> + alertEnabled_ {}; + std::vector variables_ {}; +}; + +AudioSettings::AudioSettings() : + SettingsCategory("audio"), p(std::make_unique()) +{ + RegisterVariables( + {&p->alertLocationMethod_, &p->alertLatitude_, &p->alertLongitude_}); + RegisterVariables(p->variables_); + SetDefaults(); + + p->variables_.clear(); +} +AudioSettings::~AudioSettings() = default; + +AudioSettings::AudioSettings(AudioSettings&&) noexcept = default; +AudioSettings& AudioSettings::operator=(AudioSettings&&) noexcept = default; + +SettingsVariable& AudioSettings::alert_location_method() const +{ + return p->alertLocationMethod_; +} + +SettingsVariable& AudioSettings::alert_latitude() const +{ + return p->alertLatitude_; +} + +SettingsVariable& AudioSettings::alert_longitude() const +{ + return p->alertLongitude_; +} + +SettingsVariable& +AudioSettings::alert_enabled(awips::Phenomenon phenomenon) const +{ + auto alert = p->alertEnabled_.find(phenomenon); + if (alert == p->alertEnabled_.cend()) + { + alert = p->alertEnabled_.find(kDefaultPhenomenon_); + } + return alert->second; +} + +AudioSettings& AudioSettings::Instance() +{ + static AudioSettings audioSettings_; + return audioSettings_; +} + +bool operator==(const AudioSettings& lhs, const AudioSettings& rhs) +{ + return (lhs.p->alertLocationMethod_ == rhs.p->alertLocationMethod_ && + lhs.p->alertLatitude_ == rhs.p->alertLatitude_ && + lhs.p->alertLongitude_ == rhs.p->alertLongitude_ && + lhs.p->alertEnabled_ == rhs.p->alertEnabled_); +} + +} // namespace settings +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp new file mode 100644 index 00000000..e9b111f1 --- /dev/null +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace settings +{ + +class AudioSettings : public SettingsCategory +{ +public: + explicit AudioSettings(); + ~AudioSettings(); + + AudioSettings(const AudioSettings&) = delete; + AudioSettings& operator=(const AudioSettings&) = delete; + + AudioSettings(AudioSettings&&) noexcept; + AudioSettings& operator=(AudioSettings&&) noexcept; + + SettingsVariable& alert_location_method() const; + SettingsVariable& alert_latitude() const; + SettingsVariable& alert_longitude() const; + SettingsVariable& alert_enabled(awips::Phenomenon phenomenon) const; + + static AudioSettings& Instance(); + + friend bool operator==(const AudioSettings& lhs, const AudioSettings& rhs); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace settings +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/settings/settings_definitions.hpp b/scwx-qt/source/scwx/qt/settings/settings_definitions.hpp new file mode 100644 index 00000000..b6f0d7fd --- /dev/null +++ b/scwx-qt/source/scwx/qt/settings/settings_definitions.hpp @@ -0,0 +1,20 @@ +#pragma once + +#define SCWX_SETTINGS_ENUM_VALIDATOR(Type, Iterator, ToName) \ + [](const std::string& value) \ + { \ + for (Type enumValue : Iterator) \ + { \ + /* If the value is equal to a lower case name */ \ + std::string enumName = ToName(enumValue); \ + boost::to_lower(enumName); \ + if (value == enumName) \ + { \ + /* Regard as a match, valid */ \ + return true; \ + } \ + } \ + \ + /* No match found, invalid */ \ + return false; \ + } diff --git a/test/data b/test/data index cd36a74a..85525670 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit cd36a74a9c678d90d10ec397eae65b389a9640fc +Subproject commit 85525670368987258d41f2a7b0e92266dcec9048 From c03884c2c00428c6484178e2fe015f02ff8729db Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 29 Nov 2023 22:39:40 -0600 Subject: [PATCH 09/29] Add audio page to settings dialog --- .../font-awesome-6/volume-high-solid.svg | 1 + scwx-qt/scwx-qt.qrc | 1 + .../scwx/qt/settings/settings_interface.cpp | 84 +++++++++++ scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 98 +++++++++++++ scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 135 +++++++++++++++++- 5 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 scwx-qt/res/icons/font-awesome-6/volume-high-solid.svg diff --git a/scwx-qt/res/icons/font-awesome-6/volume-high-solid.svg b/scwx-qt/res/icons/font-awesome-6/volume-high-solid.svg new file mode 100644 index 00000000..21b6c285 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/volume-high-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index 51cc16df..c094673a 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -44,6 +44,7 @@ res/icons/font-awesome-6/square-caret-right-regular.svg res/icons/font-awesome-6/square-minus-regular.svg res/icons/font-awesome-6/square-plus-regular.svg + res/icons/font-awesome-6/volume-high-solid.svg res/palettes/wct/CC.pal res/palettes/wct/Default16.pal res/palettes/wct/DOD_DSD.pal diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp index b7133537..a20e7dfa 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp @@ -180,6 +180,32 @@ void SettingsInterface::SetEditWidget(QWidget* widget) // TODO: Display invalid status }); } + else if constexpr (std::is_same_v) + { + // If the line is edited (not programatically changed), stage the new + // value + QObject::connect(lineEdit, + &QLineEdit::textEdited, + p->context_.get(), + [this](const QString& text) + { + // Convert to a double + bool ok; + double value = text.toDouble(&ok); + if (ok) + { + // Attempt to stage the value + p->stagedValid_ = + p->variable_->StageValue(value); + p->UpdateResetButton(); + } + else + { + p->stagedValid_ = false; + p->UpdateResetButton(); + } + }); + } else if constexpr (std::is_same_v>) { // If the line is edited (not programatically changed), stage the new @@ -310,6 +336,52 @@ void SettingsInterface::SetEditWidget(QWidget* widget) }); } } + else if (QDoubleSpinBox* doubleSpinBox = + dynamic_cast(widget)) + { + if constexpr (std::is_floating_point_v) + { + const std::optional minimum = p->variable_->GetMinimum(); + const std::optional maximum = p->variable_->GetMaximum(); + + if (minimum.has_value()) + { + doubleSpinBox->setMinimum(static_cast(*minimum)); + } + if (maximum.has_value()) + { + doubleSpinBox->setMaximum(static_cast(*maximum)); + } + + // If the spin box is edited, stage a changed value + QObject::connect( + doubleSpinBox, + &QDoubleSpinBox::valueChanged, + p->context_.get(), + [this](double d) + { + const T value = p->variable_->GetValue(); + const std::optional staged = p->variable_->GetStaged(); + + // If there is a value staged, and the new value is the same as + // the current value, reset the staged value + if (staged.has_value() && static_cast(d) == value) + { + p->variable_->Reset(); + p->stagedValid_ = true; + p->UpdateResetButton(); + } + // If there is no staged value, or if the new value is different + // than what is staged, attempt to stage the value + else if (!staged.has_value() || static_cast(d) != *staged) + { + p->stagedValid_ = p->variable_->StageValue(static_cast(d)); + p->UpdateResetButton(); + } + // Otherwise, don't process an unchanged value + }); + } + } p->UpdateEditWidget(); } @@ -378,6 +450,10 @@ void SettingsInterface::Impl::SetWidgetText(U* widget, const T& currentValue) { widget->setText(QString::number(currentValue)); } + else if constexpr (std::is_floating_point_v) + { + widget->setText(QString::number(currentValue)); + } else if constexpr (std::is_same_v) { if (mapFromValue_ != nullptr) @@ -448,6 +524,14 @@ void SettingsInterface::Impl::UpdateEditWidget() spinBox->setValue(static_cast(currentValue)); } } + else if (QDoubleSpinBox* doubleSpinBox = + dynamic_cast(editWidget_)) + { + if constexpr (std::is_floating_point_v) + { + doubleSpinBox->setValue(static_cast(currentValue)); + } + } } template diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index efc267bf..296e33e0 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -6,12 +6,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -84,6 +86,24 @@ static const std::unordered_map {"VIL", {0u, 255u, 1.0f, 2.5f}}, {"???", {0u, 15u, 0.0f, 1.0f}}}; +#define SCWX_ENUM_MAP_FROM_VALUE(Type, Iterator, ToName) \ + [](const std::string& text) -> std::string \ + { \ + for (Type enumValue : Iterator) \ + { \ + const std::string enumName = ToName(enumValue); \ + \ + if (boost::iequals(text, enumName)) \ + { \ + /* Return label */ \ + return enumName; \ + } \ + } \ + \ + /* Label not found, return unknown */ \ + return "?"; \ + } + class SettingsDialogImpl { public: @@ -104,6 +124,9 @@ class SettingsDialogImpl &antiAliasingEnabled_, &updateNotificationsEnabled_, &debugEnabled_, + &alertAudioLocationMethod_, + &alertAudioLatitude_, + &alertAudioLongitude_, &hoverTextWrap_, &tooltipMethod_, &placefileTextDropShadowEnabled_}} @@ -136,6 +159,7 @@ class SettingsDialogImpl void SetupGeneralTab(); void SetupPalettesColorTablesTab(); void SetupPalettesAlertsTab(); + void SetupAudioTab(); void SetupTextTab(); void ShowColorDialog(QLineEdit* lineEdit, QFrame* frame = nullptr); @@ -191,6 +215,13 @@ class SettingsDialogImpl settings::SettingsInterface> inactiveAlertColors_ {}; + settings::SettingsInterface alertAudioLocationMethod_ {}; + settings::SettingsInterface alertAudioLatitude_ {}; + settings::SettingsInterface alertAudioLongitude_ {}; + + std::unordered_map> + alertAudioEnabled_ {}; + std::unordered_map> fontFamilies_ {}; @@ -223,6 +254,9 @@ SettingsDialog::SettingsDialog(QWidget* parent) : // Palettes > Alerts p->SetupPalettesAlertsTab(); + // Audio + p->SetupAudioTab(); + // Text p->SetupTextTab(); @@ -766,6 +800,70 @@ void SettingsDialogImpl::SetupPalettesAlertsTab() } } +void SettingsDialogImpl::SetupAudioTab() +{ + settings::AudioSettings& audioSettings = settings::AudioSettings::Instance(); + + for (const auto& locationMethod : types::LocationMethodIterator()) + { + self_->ui->alertAudioLocationMethodComboBox->addItem( + QString::fromStdString(types::GetLocationMethodName(locationMethod))); + } + + alertAudioLocationMethod_.SetSettingsVariable( + audioSettings.alert_location_method()); + alertAudioLocationMethod_.SetMapFromValueFunction( + SCWX_ENUM_MAP_FROM_VALUE(types::LocationMethod, + types::LocationMethodIterator(), + types::GetLocationMethodName)); + alertAudioLocationMethod_.SetMapToValueFunction( + [](std::string text) -> std::string + { + // Convert label to lower case and return + boost::to_lower(text); + return text; + }); + alertAudioLocationMethod_.SetEditWidget( + self_->ui->alertAudioLocationMethodComboBox); + alertAudioLocationMethod_.SetResetButton( + self_->ui->resetAlertAudioLocationMethodButton); + + alertAudioLatitude_.SetSettingsVariable(audioSettings.alert_latitude()); + alertAudioLatitude_.SetEditWidget(self_->ui->alertAudioLatitudeSpinBox); + alertAudioLatitude_.SetResetButton(self_->ui->resetAlertAudioLatitudeButton); + + alertAudioLongitude_.SetSettingsVariable(audioSettings.alert_longitude()); + alertAudioLongitude_.SetEditWidget(self_->ui->alertAudioLongitudeSpinBox); + alertAudioLongitude_.SetResetButton( + self_->ui->resetAlertAudioLongitudeButton); + + auto alertAudioLayout = + static_cast(self_->ui->alertAudioGroupBox->layout()); + + for (const auto& phenomenon : types::GetAlertAudioPhenomena()) + { + QCheckBox* alertAudioCheckbox = new QCheckBox(self_); + alertAudioCheckbox->setText( + QString::fromStdString(awips::GetPhenomenonText(phenomenon))); + + static_cast(self_->ui->alertAudioGroupBox->layout()) + ->addWidget( + alertAudioCheckbox, alertAudioLayout->rowCount(), 0, 1, -1); + + // Create settings interface + auto result = alertAudioEnabled_.emplace( + phenomenon, settings::SettingsInterface {}); + auto& alertAudioEnabled = result.first->second; + + // Add to settings list + settings_.push_back(&alertAudioEnabled); + + alertAudioEnabled.SetSettingsVariable( + audioSettings.alert_enabled(phenomenon)); + alertAudioEnabled.SetEditWidget(alertAudioCheckbox); + } +} + void SettingsDialogImpl::SetupTextTab() { settings::TextSettings& textSettings = settings::TextSettings::Instance(); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index f770135f..b7be3c87 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -77,6 +77,15 @@ :/res/icons/font-awesome-6/palette-solid.svg:/res/icons/font-awesome-6/palette-solid.svg + + + Audio + + + + :/res/icons/font-awesome-6/volume-high-solid.svg:/res/icons/font-awesome-6/volume-high-solid.svg + + Text @@ -364,8 +373,8 @@ 0 0 - 63 - 18 + 508 + 383 @@ -436,6 +445,128 @@ + + + + + + Alerts + + + + + + Latitude + + + + + + + 6 + + + -90.000000000000000 + + + 90.000000000000000 + + + 0.010000000000000 + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + Longitude + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + Location Method + + + + + + + 6 + + + -180.000000000000000 + + + 180.000000000000000 + + + 0.010000000000000 + + + + + + + + 0 + 0 + + + + + + + + + + + Qt::Vertical + + + + 20 + 309 + + + + + + From ac92b53a36db41223328eb41f34e6a2ad8c69929 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 30 Nov 2023 22:51:24 -0600 Subject: [PATCH 10/29] Latitude/longitude text boxes are controlled by position manager when method is track --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 296e33e0..5bf68ddb 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,7 @@ #include #include #include +#include #include #include @@ -194,6 +196,9 @@ class SettingsDialogImpl types::FontCategory selectedFontCategory_ {types::FontCategory::Unknown}; + std::shared_ptr positionManager_ { + manager::PositionManager::Instance()}; + settings::SettingsInterface defaultRadarSite_ {}; settings::SettingsInterface gridWidth_ {}; settings::SettingsInterface gridHeight_ {}; @@ -802,6 +807,27 @@ void SettingsDialogImpl::SetupPalettesAlertsTab() void SettingsDialogImpl::SetupAudioTab() { + QObject::connect(self_->ui->alertAudioLocationMethodComboBox, + &QComboBox::currentTextChanged, + self_, + [this](const QString& text) + { + types::LocationMethod locationMethod = + types::GetLocationMethod(text.toStdString()); + + bool coordinateEntryEnabled = + locationMethod == types::LocationMethod::Fixed; + + self_->ui->alertAudioLatitudeSpinBox->setEnabled( + coordinateEntryEnabled); + self_->ui->alertAudioLongitudeSpinBox->setEnabled( + coordinateEntryEnabled); + self_->ui->resetAlertAudioLatitudeButton->setEnabled( + coordinateEntryEnabled); + self_->ui->resetAlertAudioLongitudeButton->setEnabled( + coordinateEntryEnabled); + }); + settings::AudioSettings& audioSettings = settings::AudioSettings::Instance(); for (const auto& locationMethod : types::LocationMethodIterator()) @@ -862,6 +888,28 @@ void SettingsDialogImpl::SetupAudioTab() audioSettings.alert_enabled(phenomenon)); alertAudioEnabled.SetEditWidget(alertAudioCheckbox); } + + QObject::connect( + positionManager_.get(), + &manager::PositionManager::PositionUpdated, + self_, + [this](const QGeoPositionInfo& info) + { + settings::AudioSettings& audioSettings = + settings::AudioSettings::Instance(); + + if (info.isValid() && + types::GetLocationMethod( + audioSettings.alert_location_method().GetValue()) == + types::LocationMethod::Track) + { + QGeoCoordinate coordinate = info.coordinate(); + self_->ui->alertAudioLatitudeSpinBox->setValue( + coordinate.latitude()); + self_->ui->alertAudioLongitudeSpinBox->setValue( + coordinate.longitude()); + } + }); } void SettingsDialogImpl::SetupTextTab() From 212f2700b74a0540bd6996b2ec58b7182956dc22 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 30 Nov 2023 22:52:53 -0600 Subject: [PATCH 11/29] Change latitude/longitude displayed decimals to 4, and step to ten-thousandths --- scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index b7be3c87..22cf1c32 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -463,7 +463,7 @@ - 6 + 4 -90.000000000000000 @@ -472,7 +472,7 @@ 90.000000000000000 - 0.010000000000000 + 0.000100000000000 @@ -526,7 +526,7 @@ - 6 + 4 -180.000000000000000 @@ -535,7 +535,7 @@ 180.000000000000000 - 0.010000000000000 + 0.000100000000000 From 6ec594144d746319f841903119873a9bbabea3fa Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Thu, 30 Nov 2023 22:54:29 -0600 Subject: [PATCH 12/29] Create alert manager to handle audio alert location method - Will also manage receipt of alerts for playing audio --- scwx-qt/scwx-qt.cmake | 6 +- scwx-qt/source/scwx/qt/main/main_window.cpp | 3 + .../source/scwx/qt/manager/alert_manager.cpp | 77 +++++++++++++++++++ .../source/scwx/qt/manager/alert_manager.hpp | 32 ++++++++ 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/manager/alert_manager.cpp create mode 100644 scwx-qt/source/scwx/qt/manager/alert_manager.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index b9c14701..af2339b9 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -78,7 +78,8 @@ set(SRC_GL_DRAW source/scwx/qt/gl/draw/draw_item.cpp source/scwx/qt/gl/draw/placefile_text.cpp source/scwx/qt/gl/draw/placefile_triangles.cpp source/scwx/qt/gl/draw/rectangle.cpp) -set(HDR_MANAGER source/scwx/qt/manager/font_manager.hpp +set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp + source/scwx/qt/manager/font_manager.hpp source/scwx/qt/manager/media_manager.hpp source/scwx/qt/manager/placefile_manager.hpp source/scwx/qt/manager/position_manager.hpp @@ -89,7 +90,8 @@ set(HDR_MANAGER source/scwx/qt/manager/font_manager.hpp source/scwx/qt/manager/text_event_manager.hpp source/scwx/qt/manager/timeline_manager.hpp source/scwx/qt/manager/update_manager.hpp) -set(SRC_MANAGER source/scwx/qt/manager/font_manager.cpp +set(SRC_MANAGER source/scwx/qt/manager/alert_manager.cpp + source/scwx/qt/manager/font_manager.cpp source/scwx/qt/manager/media_manager.cpp source/scwx/qt/manager/placefile_manager.cpp source/scwx/qt/manager/position_manager.cpp diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 3f4765b4..d94612be 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -82,6 +83,7 @@ class MainWindowImpl : public QObject radarSiteDialog_ {nullptr}, settingsDialog_ {nullptr}, updateDialog_ {nullptr}, + alertManager_ {manager::AlertManager::Instance()}, placefileManager_ {manager::PlacefileManager::Instance()}, positionManager_ {manager::PositionManager::Instance()}, textEventManager_ {manager::TextEventManager::Instance()}, @@ -178,6 +180,7 @@ class MainWindowImpl : public QObject ui::SettingsDialog* settingsDialog_; ui::UpdateDialog* updateDialog_; + std::shared_ptr alertManager_; std::shared_ptr placefileManager_; std::shared_ptr positionManager_; std::shared_ptr textEventManager_; diff --git a/scwx-qt/source/scwx/qt/manager/alert_manager.cpp b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp new file mode 100644 index 00000000..c7c9f7e1 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp @@ -0,0 +1,77 @@ +#include +#include +#include +#include +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +static const std::string logPrefix_ = "scwx::qt::manager::alert_manager"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class AlertManager::Impl +{ +public: + explicit Impl(AlertManager* self) : self_ {self} + { + settings::AudioSettings& audioSettings = + settings::AudioSettings::Instance(); + + UpdateLocationTracking(audioSettings.alert_location_method().GetValue()); + + audioSettings.alert_location_method().RegisterValueChangedCallback( + [this](const std::string& value) { UpdateLocationTracking(value); }); + } + + ~Impl() {} + + void UpdateLocationTracking(const std::string& value) const; + + AlertManager* self_; + + boost::uuids::uuid uuid_ {boost::uuids::random_generator()()}; + + std::shared_ptr positionManager_ { + PositionManager::Instance()}; +}; + +AlertManager::AlertManager() : p(std::make_unique(this)) {} +AlertManager::~AlertManager() = default; + +void AlertManager::Impl::UpdateLocationTracking( + const std::string& locationMethodName) const +{ + types::LocationMethod locationMethod = + types::GetLocationMethod(locationMethodName); + bool locationEnabled = locationMethod == types::LocationMethod::Track; + positionManager_->EnablePositionUpdates(uuid_, locationEnabled); +} + +std::shared_ptr AlertManager::Instance() +{ + static std::weak_ptr alertManagerReference_ {}; + static std::mutex instanceMutex_ {}; + + std::unique_lock lock(instanceMutex_); + + std::shared_ptr alertManager = alertManagerReference_.lock(); + + if (alertManager == nullptr) + { + alertManager = std::make_shared(); + alertManagerReference_ = alertManager; + } + + return alertManager; +} + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/alert_manager.hpp b/scwx-qt/source/scwx/qt/manager/alert_manager.hpp new file mode 100644 index 00000000..5bdfd923 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/alert_manager.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +class AlertManager : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(AlertManager) + +public: + explicit AlertManager(); + ~AlertManager(); + + static std::shared_ptr Instance(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace manager +} // namespace qt +} // namespace scwx From 8780da4148857bfe855f8a6c014db0c3427bc37b Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 2 Dec 2023 07:42:29 -0600 Subject: [PATCH 13/29] Determine if a geographic area contains a point --- ACKNOWLEDGEMENTS.md | 1 + conanfile.py | 4 +- scwx-qt/scwx-qt.cmake | 3 + .../source/scwx/qt/util/geographic_lib.cpp | 62 +++++++++++++++++++ .../source/scwx/qt/util/geographic_lib.hpp | 16 +++++ 5 files changed, 85 insertions(+), 1 deletion(-) diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 8b08f4ee..0435d04c 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -22,6 +22,7 @@ Supercell Wx uses code from the following dependencies: | [FreeType](https://freetype.org/) | [Freetype Project License](https://spdx.org/licenses/FTL.html) | | [FreeType GL](https://github.com/rougier/freetype-gl) | [BSD 2-Clause with views sentence](https://spdx.org/licenses/BSD-2-Clause-Views.html) | | [GeographicLib](https://geographiclib.sourceforge.io/) | [MIT License](https://spdx.org/licenses/MIT.html) | +| [geos](https://libgeos.org/) | [GNU Lesser General Public License v2.1 or later](https://spdx.org/licenses/LGPL-2.1-or-later.html) | | [GLEW](https://www.opengl.org/sdk/libs/GLEW/) | [MIT License](https://spdx.org/licenses/MIT.html) | | [GLM](https://github.com/g-truc/glm) | [MIT License](https://spdx.org/licenses/MIT.html) | | [GoogleTest](https://google.github.io/googletest/) | [BSD 3-Clause "New" or "Revised" License](https://spdx.org/licenses/BSD-3-Clause.html) | diff --git a/conanfile.py b/conanfile.py index fdfae57a..8ffb23f5 100644 --- a/conanfile.py +++ b/conanfile.py @@ -6,6 +6,7 @@ class SupercellWxConan(ConanFile): "cpr/1.10.5", "fontconfig/2.14.2", "geographiclib/2.3", + "geos/3.12.0", "glew/2.2.0", "glm/cci.20230113", "gtest/1.14.0", @@ -19,7 +20,8 @@ class SupercellWxConan(ConanFile): generators = ("cmake", "cmake_find_package", "cmake_paths") - default_options = {"libiconv:shared" : True, + default_options = {"geos:shared" : True, + "libiconv:shared" : True, "openssl:no_module": True, "openssl:shared" : True} diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index af2339b9..44e0c0ae 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -14,6 +14,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Boost) find_package(Fontconfig) find_package(geographiclib) +find_package(geos) find_package(GLEW) find_package(glm) find_package(Python COMPONENTS Interpreter) @@ -533,6 +534,8 @@ target_link_libraries(scwx-qt PUBLIC Qt${QT_VERSION_MAJOR}::Widgets $<$:opengl32> Fontconfig::Fontconfig GeographicLib::GeographicLib + GEOS::geos + GEOS::geos_cxx_flags GLEW::GLEW glm::glm imgui diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp index 6718715a..1736f6a2 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp @@ -1,4 +1,9 @@ #include +#include + +#include +#include +#include namespace scwx { @@ -9,6 +14,9 @@ namespace util namespace GeographicLib { +static const std::string logPrefix_ = "scwx::qt::util::geographic_lib"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + const ::GeographicLib::Geodesic& DefaultGeodesic() { static const ::GeographicLib::Geodesic geodesic_ { @@ -18,6 +26,60 @@ const ::GeographicLib::Geodesic& DefaultGeodesic() return geodesic_; } +bool AreaContainsPoint(const std::vector& area, + const common::Coordinate& point) +{ + // Cannot have an area with just two points + if (area.size() <= 2 || area.size() == 3 && area.front() == area.back()) + { + return false; + } + + ::GeographicLib::Gnomonic gnomonic {}; + geos::geom::CoordinateSequence sequence {}; + double x; + double y; + bool areaContainsPoint = false; + + // Using a gnomonic projection with the test point as the center + // latitude/longitude, the projected test point will be at (0, 0) + geos::geom::CoordinateXY zero {}; + + // Create the area coordinate sequence using a gnomonic projection + for (auto& areaCoordinate : area) + { + gnomonic.Forward(point.latitude_, + point.longitude_, + areaCoordinate.latitude_, + areaCoordinate.longitude_, + x, + y); + sequence.add(x, y); + } + + // If the sequence is not a ring, add the first point again for closure + if (!sequence.isRing()) + { + sequence.add(sequence.front(), false); + } + + // The sequence should be a ring at this point, but make sure + if (sequence.isRing()) + { + try + { + areaContainsPoint = + geos::algorithm::PointLocation::isInRing(zero, &sequence); + } + catch (const std::exception&) + { + logger_->trace("Invalid area sequence"); + } + } + + return areaContainsPoint; +} + units::angle::degrees GetAngle(double lat1, double lon1, double lat2, double lon2) { diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.hpp b/scwx-qt/source/scwx/qt/util/geographic_lib.hpp index d03aac04..66b4f42b 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.hpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.hpp @@ -1,5 +1,9 @@ #pragma once +#include + +#include + #include #include #include @@ -20,6 +24,18 @@ namespace GeographicLib */ const ::GeographicLib::Geodesic& DefaultGeodesic(); +/** + * Determine if an area/ring, oriented in either direction, contains a point. A + * point lying on the area boundary is considered to be inside the area. + * + * @param [in] area A vector of Coordinates representing the area + * @param [in] point The point to check against the area + * + * @return true if point is inside the area + */ +bool AreaContainsPoint(const std::vector& area, + const common::Coordinate& point); + /** * Get the angle between two points. * From dd79f9208dd034323056ae9776be75d05ce841bf Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 2 Dec 2023 07:42:43 -0600 Subject: [PATCH 14/29] Update acknowledgements for Qt plugins --- ACKNOWLEDGEMENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 0435d04c..27f6686e 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -34,7 +34,7 @@ Supercell Wx uses code from the following dependencies: | [MapLibre Native](https://maplibre.org/projects/maplibre-native/) | [BSD 2-Clause "Simplified" License](https://spdx.org/licenses/BSD-2-Clause.html) | | [nunicode](https://bitbucket.org/alekseyt/nunicode/src/master/) | [MIT License](https://spdx.org/licenses/MIT.html) | Modified for MapLibre Native | | [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 Network, Qt OpenGL, Qt SQL, Qt SVG, Qt Widgets
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 SQL, Qt SVG, Qt Widgets
Additional Licenses: https://doc.qt.io/qt-6/licenses-used-in-qt.html | | [spdlog](https://github.com/gabime/spdlog) | [MIT License](https://spdx.org/licenses/MIT.html) | | [SQLite](https://www.sqlite.org/) | Public Domain | | [stb](https://github.com/nothings/stb) | Public Domain | From 40fc8ade20215e619b09e4c645cb8bb248bf4184 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 2 Dec 2023 07:44:27 -0600 Subject: [PATCH 15/29] Handle received alerts and test against location for playing alert audio --- .../source/scwx/qt/manager/alert_manager.cpp | 104 +++++++++++++++++- .../scwx/qt/settings/audio_settings.cpp | 7 +- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/alert_manager.cpp b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp index c7c9f7e1..93d4395f 100644 --- a/scwx-qt/source/scwx/qt/manager/alert_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp @@ -1,10 +1,16 @@ #include +#include #include +#include #include #include +#include #include +#include +#include #include +#include namespace scwx { @@ -28,23 +34,119 @@ class AlertManager::Impl audioSettings.alert_location_method().RegisterValueChangedCallback( [this](const std::string& value) { UpdateLocationTracking(value); }); + + QObject::connect( + textEventManager_.get(), + &manager::TextEventManager::AlertUpdated, + self_, + [this](const types::TextEventKey& key, size_t messageIndex) + { + boost::asio::post(threadPool_, + [=, this]() { HandleAlert(key, messageIndex); }); + }); } - ~Impl() {} + ~Impl() { threadPool_.join(); } + common::Coordinate CurrentCoordinate() const; + void HandleAlert(const types::TextEventKey& key, size_t messageIndex) const; void UpdateLocationTracking(const std::string& value) const; + boost::asio::thread_pool threadPool_ {1u}; + AlertManager* self_; boost::uuids::uuid uuid_ {boost::uuids::random_generator()()}; + std::shared_ptr mediaManager_ {MediaManager::Instance()}; std::shared_ptr positionManager_ { PositionManager::Instance()}; + std::shared_ptr textEventManager_ { + TextEventManager::Instance()}; }; AlertManager::AlertManager() : p(std::make_unique(this)) {} AlertManager::~AlertManager() = default; +common::Coordinate AlertManager::Impl::CurrentCoordinate() const +{ + settings::AudioSettings& audioSettings = settings::AudioSettings::Instance(); + common::Coordinate coordinate {}; + + types::LocationMethod locationMethod = types::GetLocationMethod( + audioSettings.alert_location_method().GetValue()); + + if (locationMethod == types::LocationMethod::Fixed) + { + coordinate.latitude_ = audioSettings.alert_latitude().GetValue(); + coordinate.longitude_ = audioSettings.alert_longitude().GetValue(); + } + else if (locationMethod == types::LocationMethod::Track) + { + QGeoPositionInfo position = positionManager_->position(); + if (position.isValid()) + { + QGeoCoordinate trackedCoordinate = position.coordinate(); + coordinate.latitude_ = trackedCoordinate.latitude(); + coordinate.longitude_ = trackedCoordinate.longitude(); + } + } + + return coordinate; +} + +void AlertManager::Impl::HandleAlert(const types::TextEventKey& key, + size_t messageIndex) const +{ + // Skip alert if there are more messages to be processed + if (messageIndex + 1 < textEventManager_->message_count(key)) + { + return; + } + + settings::AudioSettings& audioSettings = settings::AudioSettings::Instance(); + common::Coordinate currentCoordinate = CurrentCoordinate(); + + auto message = textEventManager_->message_list(key).at(messageIndex); + + for (auto& segment : message->segments()) + { + if (!segment->codedLocation_.has_value()) + { + continue; + } + + auto& vtec = segment->header_->vtecString_.front(); + auto action = vtec.pVtec_.action(); + awips::Phenomenon phenomenon = vtec.pVtec_.phenomenon(); + auto eventEnd = vtec.pVtec_.event_end(); + bool alertActive = (action != awips::PVtec::Action::Canceled); + + // If the event has ended or is inactive, or if the alert is not enabled, + // skip it + if (eventEnd < std::chrono::system_clock::now() || !alertActive || + !audioSettings.alert_enabled(phenomenon).GetValue()) + { + continue; + } + + // Determine if the alert is active at the current coordinte + auto alertCoordinates = segment->codedLocation_->coordinates(); + + if (util::GeographicLib::AreaContainsPoint(alertCoordinates, + currentCoordinate)) + { + logger_->info("Alert active at current location: {} {}.{} {}", + vtec.pVtec_.office_id(), + awips::GetPhenomenonCode(vtec.pVtec_.phenomenon()), + awips::PVtec::GetActionCode(vtec.pVtec_.action()), + vtec.pVtec_.event_tracking_number()); + + mediaManager_->Play(types::AudioFile::EasAttentionSignal); + } + } +} + void AlertManager::Impl::UpdateLocationTracking( const std::string& locationMethodName) const { diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp index 869973ae..2d2844cd 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp @@ -17,8 +17,7 @@ namespace settings static const std::string logPrefix_ = "scwx::qt::settings::audio_settings"; static const bool kDefaultAlertEnabled_ {false}; -static const awips::Phenomenon kDefaultPhenomenon_ { - awips::Phenomenon::FlashFlood}; +static const awips::Phenomenon kDefaultPhenomenon_ {awips::Phenomenon::Unknown}; class AudioSettings::Impl { @@ -58,6 +57,10 @@ class AudioSettings::Impl variables_.push_back(&variable); } + + // Create a default disabled alert, not stored in the settings file + alertEnabled_.emplace(kDefaultPhenomenon_, + SettingsVariable {"alert_disabled"}); } ~Impl() {} From a495cf1b3b401c5e3d4af102e96fd542b8598df0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 2 Dec 2023 23:02:43 -0600 Subject: [PATCH 16/29] Add alert sound file to settings --- .../source/scwx/qt/manager/media_manager.cpp | 3 ++- .../scwx/qt/settings/audio_settings.cpp | 19 ++++++++++++++++--- .../scwx/qt/settings/audio_settings.hpp | 1 + scwx-qt/source/scwx/qt/types/media_types.cpp | 2 +- test/data | 2 +- 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/media_manager.cpp b/scwx-qt/source/scwx/qt/manager/media_manager.cpp index 53caba8c..60457fbc 100644 --- a/scwx-qt/source/scwx/qt/manager/media_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/media_manager.cpp @@ -84,7 +84,8 @@ void MediaManager::Play(types::AudioFile media) logger_->debug("Playing audio: {}", path); - p->mediaPlayer_->setSource(QUrl(QString::fromStdString(path))); + p->mediaPlayer_->setSource( + QUrl(QString("qrc:%1").arg(QString::fromStdString(path)))); QMetaObject::invokeMethod(p->mediaPlayer_, &QMediaPlayer::play); } diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp index 2d2844cd..f5719525 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -24,11 +25,14 @@ class AudioSettings::Impl public: explicit Impl() { + std::string defaultAlertSoundFileValue = + types::GetMediaPath(types::AudioFile::EasAttentionSignal); std::string defaultAlertLocationMethodValue = types::GetLocationMethodName(types::LocationMethod::Fixed); boost::to_lower(defaultAlertLocationMethodValue); + alertSoundFile_.SetDefault(defaultAlertSoundFileValue); alertLocationMethod_.SetDefault(defaultAlertLocationMethodValue); alertLatitude_.SetDefault(0.0); alertLongitude_.SetDefault(0.0); @@ -65,6 +69,7 @@ class AudioSettings::Impl ~Impl() {} + SettingsVariable alertSoundFile_ {"alert_sound_file"}; SettingsVariable alertLocationMethod_ {"alert_location_method"}; SettingsVariable alertLatitude_ {"alert_latitude"}; SettingsVariable alertLongitude_ {"alert_longitude"}; @@ -77,8 +82,10 @@ class AudioSettings::Impl AudioSettings::AudioSettings() : SettingsCategory("audio"), p(std::make_unique()) { - RegisterVariables( - {&p->alertLocationMethod_, &p->alertLatitude_, &p->alertLongitude_}); + RegisterVariables({&p->alertSoundFile_, + &p->alertLocationMethod_, + &p->alertLatitude_, + &p->alertLongitude_}); RegisterVariables(p->variables_); SetDefaults(); @@ -89,6 +96,11 @@ AudioSettings::~AudioSettings() = default; AudioSettings::AudioSettings(AudioSettings&&) noexcept = default; AudioSettings& AudioSettings::operator=(AudioSettings&&) noexcept = default; +SettingsVariable& AudioSettings::alert_sound_file() const +{ + return p->alertSoundFile_; +} + SettingsVariable& AudioSettings::alert_location_method() const { return p->alertLocationMethod_; @@ -123,7 +135,8 @@ AudioSettings& AudioSettings::Instance() bool operator==(const AudioSettings& lhs, const AudioSettings& rhs) { - return (lhs.p->alertLocationMethod_ == rhs.p->alertLocationMethod_ && + return (lhs.p->alertSoundFile_ == rhs.p->alertSoundFile_ && + lhs.p->alertLocationMethod_ == rhs.p->alertLocationMethod_ && lhs.p->alertLatitude_ == rhs.p->alertLatitude_ && lhs.p->alertLongitude_ == rhs.p->alertLongitude_ && lhs.p->alertEnabled_ == rhs.p->alertEnabled_); diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp index e9b111f1..e332cc8d 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp @@ -26,6 +26,7 @@ class AudioSettings : public SettingsCategory AudioSettings(AudioSettings&&) noexcept; AudioSettings& operator=(AudioSettings&&) noexcept; + SettingsVariable& alert_sound_file() const; SettingsVariable& alert_location_method() const; SettingsVariable& alert_latitude() const; SettingsVariable& alert_longitude() const; diff --git a/scwx-qt/source/scwx/qt/types/media_types.cpp b/scwx-qt/source/scwx/qt/types/media_types.cpp index a8279e73..9e3c13ca 100644 --- a/scwx-qt/source/scwx/qt/types/media_types.cpp +++ b/scwx-qt/source/scwx/qt/types/media_types.cpp @@ -11,7 +11,7 @@ namespace types static const std::unordered_map audioFileInfo_ { {AudioFile::EasAttentionSignal, - "qrc:/res/audio/wikimedia/" + ":/res/audio/wikimedia/" "Emergency_Alert_System_Attention_Signal_20s.ogg"}}; const std::string& GetMediaPath(AudioFile audioFile) diff --git a/test/data b/test/data index 85525670..8a633285 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 85525670368987258d41f2a7b0e92266dcec9048 +Subproject commit 8a633285ecbe8ff7d48631485620a7c7a5257229 From 2345855a97c448492409fe9a6cfb13702d82c988 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 2 Dec 2023 23:26:54 -0600 Subject: [PATCH 17/29] Add alert audio sound to settings dialog --- .../res/icons/font-awesome-6/stop-solid.svg | 1 + scwx-qt/scwx-qt.qrc | 1 + .../source/scwx/qt/manager/media_manager.cpp | 24 ++++- .../source/scwx/qt/manager/media_manager.hpp | 2 + scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 23 +++++ scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 94 ++++++++++++++----- 6 files changed, 117 insertions(+), 28 deletions(-) create mode 100644 scwx-qt/res/icons/font-awesome-6/stop-solid.svg diff --git a/scwx-qt/res/icons/font-awesome-6/stop-solid.svg b/scwx-qt/res/icons/font-awesome-6/stop-solid.svg new file mode 100644 index 00000000..778163e8 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/stop-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index c094673a..53d7aec2 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -44,6 +44,7 @@ res/icons/font-awesome-6/square-caret-right-regular.svg res/icons/font-awesome-6/square-minus-regular.svg res/icons/font-awesome-6/square-plus-regular.svg + res/icons/font-awesome-6/stop-solid.svg res/icons/font-awesome-6/volume-high-solid.svg res/palettes/wct/CC.pal res/palettes/wct/Default16.pal diff --git a/scwx-qt/source/scwx/qt/manager/media_manager.cpp b/scwx-qt/source/scwx/qt/manager/media_manager.cpp index 60457fbc..349e73b9 100644 --- a/scwx-qt/source/scwx/qt/manager/media_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/media_manager.cpp @@ -81,15 +81,33 @@ void MediaManager::Impl::ConnectSignals() void MediaManager::Play(types::AudioFile media) { const std::string path = types::GetMediaPath(media); +} + +void MediaManager::Play(const std::string& mediaPath) +{ + logger_->debug("Playing audio: {}", mediaPath); - logger_->debug("Playing audio: {}", path); + if (mediaPath.starts_with(':')) + { + p->mediaPlayer_->setSource( + QUrl(QString("qrc%1").arg(QString::fromStdString(mediaPath)))); + } + else + { + p->mediaPlayer_->setSource( + QUrl::fromLocalFile(QString::fromStdString(mediaPath))); + } - p->mediaPlayer_->setSource( - QUrl(QString("qrc:%1").arg(QString::fromStdString(path)))); + p->mediaPlayer_->setPosition(0); QMetaObject::invokeMethod(p->mediaPlayer_, &QMediaPlayer::play); } +void MediaManager::Stop() +{ + QMetaObject::invokeMethod(p->mediaPlayer_, &QMediaPlayer::stop); +} + std::shared_ptr MediaManager::Instance() { static std::weak_ptr mediaManagerReference_ {}; diff --git a/scwx-qt/source/scwx/qt/manager/media_manager.hpp b/scwx-qt/source/scwx/qt/manager/media_manager.hpp index 1b6f7ce9..f1d73656 100644 --- a/scwx-qt/source/scwx/qt/manager/media_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/media_manager.hpp @@ -23,6 +23,8 @@ class MediaManager : public QObject ~MediaManager(); void Play(types::AudioFile media); + void Play(const std::string& mediaPath); + void Stop(); static std::shared_ptr Instance(); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 5bf68ddb..fe8a9dfc 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -126,6 +127,7 @@ class SettingsDialogImpl &antiAliasingEnabled_, &updateNotificationsEnabled_, &debugEnabled_, + &alertAudioSoundFile_, &alertAudioLocationMethod_, &alertAudioLatitude_, &alertAudioLongitude_, @@ -196,6 +198,8 @@ class SettingsDialogImpl types::FontCategory selectedFontCategory_ {types::FontCategory::Unknown}; + std::shared_ptr mediaManager_ { + manager::MediaManager::Instance()}; std::shared_ptr positionManager_ { manager::PositionManager::Instance()}; @@ -220,6 +224,7 @@ class SettingsDialogImpl settings::SettingsInterface> inactiveAlertColors_ {}; + settings::SettingsInterface alertAudioSoundFile_ {}; settings::SettingsInterface alertAudioLocationMethod_ {}; settings::SettingsInterface alertAudioLatitude_ {}; settings::SettingsInterface alertAudioLongitude_ {}; @@ -309,6 +314,20 @@ void SettingsDialogImpl::ConnectSignals() [this](const std::string& newValue) { UpdateRadarDialogLocation(newValue); }); + QObject::connect( + self_->ui->alertAudioSoundTestButton, + &QAbstractButton::clicked, + self_, + [this]() + { + mediaManager_->Play( + self_->ui->alertAudioSoundLineEdit->text().toStdString()); + }); + QObject::connect(self_->ui->alertAudioSoundStopButton, + &QAbstractButton::clicked, + self_, + [this]() { mediaManager_->Stop(); }); + QObject::connect( self_->ui->fontListView->selectionModel(), &QItemSelectionModel::selectionChanged, @@ -830,6 +849,10 @@ void SettingsDialogImpl::SetupAudioTab() settings::AudioSettings& audioSettings = settings::AudioSettings::Instance(); + alertAudioSoundFile_.SetSettingsVariable(audioSettings.alert_sound_file()); + alertAudioSoundFile_.SetEditWidget(self_->ui->alertAudioSoundLineEdit); + alertAudioSoundFile_.SetResetButton(self_->ui->resetAlertAudioSoundButton); + for (const auto& locationMethod : types::LocationMethodIterator()) { self_->ui->alertAudioLocationMethodComboBox->addItem( diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index 22cf1c32..681923f5 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -453,14 +453,14 @@ Alerts - + Latitude - + 4 @@ -476,8 +476,8 @@ - - + + ... @@ -487,14 +487,44 @@ - + Longitude - + + + + ... + + + + + + + Location Method + + + + + + + 4 + + + -180.000000000000000 + + + 180.000000000000000 + + + 0.000100000000000 + + + + ... @@ -505,8 +535,8 @@ - - + + ... @@ -516,30 +546,36 @@ - - + + + + + - Location Method + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - 4 - - - -180.000000000000000 - - - 180.000000000000000 + + + + + :/res/icons/font-awesome-6/play-solid.svg:/res/icons/font-awesome-6/play-solid.svg - - 0.000100000000000 + + + + + + Sound - + @@ -549,6 +585,14 @@ + + + + + :/res/icons/font-awesome-6/stop-solid.svg:/res/icons/font-awesome-6/stop-solid.svg + + +
@@ -560,7 +604,7 @@ 20 - 309 + 281 From c5b2eb8ebfc132ec2e7e673d58be043d14e6b333 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sat, 2 Dec 2023 23:28:15 -0600 Subject: [PATCH 18/29] Use audio file from settings instead of default alert sound --- scwx-qt/source/scwx/qt/manager/alert_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/manager/alert_manager.cpp b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp index 93d4395f..3bbf1d31 100644 --- a/scwx-qt/source/scwx/qt/manager/alert_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp @@ -142,7 +142,7 @@ void AlertManager::Impl::HandleAlert(const types::TextEventKey& key, awips::PVtec::GetActionCode(vtec.pVtec_.action()), vtec.pVtec_.event_tracking_number()); - mediaManager_->Play(types::AudioFile::EasAttentionSignal); + mediaManager_->Play(audioSettings.alert_sound_file().GetValue()); } } } From ffd2fa83adf32931c3f5e313b36ac145e3dc1a3a Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 3 Dec 2023 06:24:32 -0600 Subject: [PATCH 19/29] Add file dialog for alert sound setting --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index fe8a9dfc..4f983a68 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -853,6 +853,47 @@ void SettingsDialogImpl::SetupAudioTab() alertAudioSoundFile_.SetEditWidget(self_->ui->alertAudioSoundLineEdit); alertAudioSoundFile_.SetResetButton(self_->ui->resetAlertAudioSoundButton); + QObject::connect( + self_->ui->alertAudioSoundSelectButton, + &QAbstractButton::clicked, + self_, + [this]() + { + static const std::string audioFilter = + "Audio Files (*.3ga *.669 *.a52 *.aac *.ac3 *.adt *.adts *.aif " + "*.aifc *.aiff *.amb *.amr *.aob *.ape *.au *.awb *.caf *.dts " + "*.flac *.it *.kar *.m4a *.m4b *.m4p *.m5p *.mid *.mka *.mlp *.mod " + "*.mpa *.mp1 *.mp2 *.mp3 *.mpc *.mpga *.mus *.oga *.ogg *.oma " + "*.opus *.qcp *.ra *.rmi *.s3m *.sid *.spx *.tak *.thd *.tta *.voc " + "*.vqf *.w64 *.wav *.wma *.wv *.xa *.xm)"; + static const std::string allFilter = "All Files (*)"; + + QFileDialog* dialog = new QFileDialog(self_); + + dialog->setFileMode(QFileDialog::ExistingFile); + dialog->setNameFilters( + {QObject::tr(audioFilter.c_str()), QObject::tr(allFilter.c_str())}); + dialog->setAttribute(Qt::WA_DeleteOnClose); + + QObject::connect( + dialog, + &QFileDialog::fileSelected, + self_, + [this](const QString& file) + { + QString path = QDir::toNativeSeparators(file); + + logger_->info("Selected alert sound file: {}", + path.toStdString()); + self_->ui->alertAudioSoundLineEdit->setText(path); + + // setText does not emit the textEdited signal + Q_EMIT self_->ui->alertAudioSoundLineEdit->textEdited(path); + }); + + dialog->open(); + }); + for (const auto& locationMethod : types::LocationMethodIterator()) { self_->ui->alertAudioLocationMethodComboBox->addItem( From a56b7400a4d46097f459a6a90eae42ecb60b715f Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 3 Dec 2023 07:16:12 -0600 Subject: [PATCH 20/29] Add states and territories to county database --- data | 2 +- scwx-qt/scwx-qt.cmake | 7 ++- .../source/scwx/qt/config/county_database.cpp | 49 ++++++++++++++++--- scwx-qt/tools/generate_counties_db.py | 32 +++++++++++- 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/data b/data index 9b6c72f8..db52049e 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 9b6c72f847193bc29d3ff183b206f26a9b5c007e +Subproject commit db52049ea651fea92b06e5024cbff3a3d3d26bc8 diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 44e0c0ae..b032971e 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -323,6 +323,7 @@ set(ZONE_DBF_FILES ${SCWX_DIR}/data/db/fz19se23.dbf ${SCWX_DIR}/data/db/mz19se23.dbf ${SCWX_DIR}/data/db/oz08mr23.dbf ${SCWX_DIR}/data/db/z_19se23.dbf) +set(STATE_DBF_FILES ${SCWX_DIR}/data/db/s_08mr23.dbf) set(COUNTIES_SQLITE_DB ${scwx-qt_BINARY_DIR}/res/db/counties.db) set(VERSIONS_INPUT ${scwx-qt_SOURCE_DIR}/source/scwx/qt/main/versions.hpp.in) @@ -411,8 +412,12 @@ add_custom_command(OUTPUT ${COUNTIES_SQLITE_DB} ${scwx-qt_SOURCE_DIR}/tools/generate_counties_db.py -c ${COUNTY_DBF_FILES} -z ${ZONE_DBF_FILES} + -s ${STATE_DBF_FILES} -o ${COUNTIES_SQLITE_DB} - DEPENDS ${COUNTY_DB_FILES} ${ZONE_DBF_FILES}) + DEPENDS ${scwx-qt_SOURCE_DIR}/tools/generate_counties_db.py + ${COUNTY_DB_FILES} + ${STATE_DBF_FILES} + ${ZONE_DBF_FILES}) add_custom_target(scwx-qt_generate_counties_db ALL DEPENDS ${COUNTIES_SQLITE_DB}) diff --git a/scwx-qt/source/scwx/qt/config/county_database.cpp b/scwx-qt/source/scwx/qt/config/county_database.cpp index a0d7b51c..a35db04f 100644 --- a/scwx-qt/source/scwx/qt/config/county_database.cpp +++ b/scwx-qt/source/scwx/qt/config/county_database.cpp @@ -28,6 +28,7 @@ static const std::string countyDatabaseFilename_ = ":/res/db/counties.db"; static bool initialized_ {false}; static std::unordered_map countyMap_; static std::shared_mutex countyMutex_; +static std::unordered_map stateMap_; void Initialize() { @@ -108,7 +109,41 @@ void Initialize() else { logger_->error( - "Database format error, invalid number of columns: {}", columns); + "County database format error, invalid number of columns: {}", + columns); + status = -1; + } + + return status; + }, + nullptr, + &errorMessage); + if (rc != SQLITE_OK) + { + logger_->error("SQL error: {}", errorMessage); + sqlite3_free(errorMessage); + } + + // Query database for states + rc = sqlite3_exec( + db, + "SELECT * FROM states", + [](void* /* param */, + int columns, + char** columnText, + char** /* columnName */) -> int + { + int status = 0; + + if (columns == 2) + { + stateMap_.emplace(columnText[0], columnText[1]); + } + else + { + logger_->error( + "State database format error, invalid number of columns: {}", + columns); status = -1; } @@ -129,13 +164,11 @@ void Initialize() sqlite3_close(db); // Remove temporary file - std::error_code err; - - if (!std::filesystem::remove(countyDatabaseCache, err)) { - logger_->warn( - "Unable to remove cached copy of database, error code: {} error category: {}", - err.value(), - err.category().name()); + std::error_code error; + if (!std::filesystem::remove(countyDatabaseCache, error)) + { + logger_->warn("Unable to remove cached copy of database: {}", + error.message()); } initialized_ = true; diff --git a/scwx-qt/tools/generate_counties_db.py b/scwx-qt/tools/generate_counties_db.py index 1cbc01bf..6605ef76 100644 --- a/scwx-qt/tools/generate_counties_db.py +++ b/scwx-qt/tools/generate_counties_db.py @@ -26,6 +26,14 @@ def ParseArguments(): nargs = "+", default = [], type = pathlib.Path) + parser.add_argument("-s", "--state_dbf", + metavar = "filename", + help = "input state database", + dest = "inputStateDbs_", + action = "extend", + nargs = "+", + default = [], + type = pathlib.Path) parser.add_argument("-o", "--output_db", metavar = "filename", help = "output sqlite database", @@ -47,10 +55,13 @@ def Prepare(dbInfo, outputDb): dbInfo.sqlCursor_ = dbInfo.sqlConnection_.cursor() - # Create database table + # Create database tables dbInfo.sqlCursor_.execute("""CREATE TABLE counties( id TEXT NOT NULL PRIMARY KEY, name TEXT)""") + dbInfo.sqlCursor_.execute("""CREATE TABLE states( + state TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL)""") def ProcessCountiesDbf(dbInfo, dbfFilename): # County area type @@ -72,6 +83,22 @@ def ProcessCountiesDbf(dbInfo, dbfFilename): except: print("Skipping duplicate county:", fipsId, row.COUNTYNAME) +def ProcessStateDbf(dbInfo, dbfFilename): + print("Processing states and territories file:", dbfFilename) + + # Read dataframe + dbfTable = gpd.read_file(filename = dbfFilename, + include_fields = ["STATE", "NAME"], + ignore_geometry = True) + dbfTable.drop_duplicates(inplace=True) + + for row in dbfTable.itertuples(): + # Insert data into database + try: + dbInfo.sqlCursor_.execute("INSERT INTO states VALUES (?, ?)", (row.STATE, row.NAME)) + except: + print("Error inserting row:", row.STATE, row.NAME) + def ProcessZoneDbf(dbInfo, dbfFilename): print("Processing zone file:", dbfFilename) # Zone area type @@ -118,4 +145,7 @@ def PostProcess(dbInfo): for zoneDb in args.inputZoneDbs_: ProcessZoneDbf(dbInfo, zoneDb) +for stateDb in args.inputStateDbs_: + ProcessStateDbf(dbInfo, stateDb) + PostProcess(dbInfo) From 7cf2121b8ed9c8a40d500ed3699b6fc12a896c96 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Sun, 3 Dec 2023 09:37:27 -0600 Subject: [PATCH 21/29] Fix two point area test --- scwx-qt/source/scwx/qt/util/geographic_lib.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp index 1736f6a2..8582d21c 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp @@ -30,7 +30,7 @@ bool AreaContainsPoint(const std::vector& area, const common::Coordinate& point) { // Cannot have an area with just two points - if (area.size() <= 2 || area.size() == 3 && area.front() == area.back()) + if (area.size() <= 2 || (area.size() == 3 && area.front() == area.back())) { return false; } From bcc7391a19e6582d7e873a4a534e9dffccd6fd22 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Wed, 6 Dec 2023 06:20:00 -0600 Subject: [PATCH 22/29] Add GetCounties to county database - Also remove mutex, as the database is only modified on initialization --- .../source/scwx/qt/config/county_database.cpp | 70 ++++++++++++++----- .../source/scwx/qt/config/county_database.hpp | 3 + .../scwx/qt/config/county_database.test.cpp | 23 ++++++ 3 files changed, 80 insertions(+), 16 deletions(-) diff --git a/scwx-qt/source/scwx/qt/config/county_database.cpp b/scwx-qt/source/scwx/qt/config/county_database.cpp index a35db04f..de968474 100644 --- a/scwx-qt/source/scwx/qt/config/county_database.cpp +++ b/scwx-qt/source/scwx/qt/config/county_database.cpp @@ -1,7 +1,6 @@ #include #include -#include #include #include @@ -25,9 +24,12 @@ static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static const std::string countyDatabaseFilename_ = ":/res/db/counties.db"; +typedef std::unordered_map CountyMap; +typedef std::unordered_map StateMap; +typedef std::unordered_map FormatMap; + static bool initialized_ {false}; -static std::unordered_map countyMap_; -static std::shared_mutex countyMutex_; +static FormatMap countyDatabase_; static std::unordered_map stateMap_; void Initialize() @@ -88,8 +90,8 @@ void Initialize() return; } - // Database is open, acquire lock - std::unique_lock lock(countyMutex_); + // Ensure counties exists + countyDatabase_.emplace('C', StateMap {}); // Query database for counties rc = sqlite3_exec( @@ -102,17 +104,26 @@ void Initialize() { int status = 0; - if (columns == 2) + if (columns == 2 && std::strlen(columnText[0]) == 6) { - countyMap_.emplace(columnText[0], columnText[1]); + std::string fipsId = columnText[0]; + std::string state = fipsId.substr(0, 2); + char type = fipsId.at(2); + + countyDatabase_[type][state].emplace(fipsId, columnText[1]); } - else + else if (columns != 2) { logger_->error( "County database format error, invalid number of columns: {}", columns); status = -1; } + else + { + logger_->error("Invalid FIPS ID: {}", columnText[0]); + status = -1; + } return status; }, @@ -157,9 +168,6 @@ void Initialize() sqlite3_free(errorMessage); } - // Finished populating county map, release lock - lock.unlock(); - // Close database sqlite3_close(db); @@ -176,17 +184,47 @@ void Initialize() std::string GetCountyName(const std::string& id) { - std::shared_lock lock(countyMutex_); - - auto it = countyMap_.find(id); - if (it != countyMap_.cend()) + if (id.length() > 3) { - return it->second; + // SSFNNN + char format = id.at(2); + std::string state = id.substr(0, 2); + + auto stateIt = countyDatabase_.find(format); + if (stateIt != countyDatabase_.cend()) + { + StateMap& states = stateIt->second; + auto countyIt = states.find(state); + if (countyIt != states.cend()) + { + CountyMap& counties = countyIt->second; + auto it = counties.find(id); + if (it != counties.cend()) + { + return it->second; + } + } + } } return id; } +std::unordered_map +GetCounties(const std::string& state) +{ + std::unordered_map counties {}; + + StateMap& states = countyDatabase_.at('C'); + auto it = states.find(state); + if (it != states.cend()) + { + counties = it->second; + } + + return counties; +} + } // namespace CountyDatabase } // namespace config } // namespace qt diff --git a/scwx-qt/source/scwx/qt/config/county_database.hpp b/scwx-qt/source/scwx/qt/config/county_database.hpp index e75431ac..5cc92d3d 100644 --- a/scwx-qt/source/scwx/qt/config/county_database.hpp +++ b/scwx-qt/source/scwx/qt/config/county_database.hpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace scwx @@ -15,6 +16,8 @@ namespace CountyDatabase void Initialize(); std::string GetCountyName(const std::string& id); +std::unordered_map +GetCounties(const std::string& state); } // namespace CountyDatabase } // namespace config diff --git a/test/source/scwx/qt/config/county_database.test.cpp b/test/source/scwx/qt/config/county_database.test.cpp index d9f0b3e2..1ef31353 100644 --- a/test/source/scwx/qt/config/county_database.test.cpp +++ b/test/source/scwx/qt/config/county_database.test.cpp @@ -15,6 +15,12 @@ class CountyDatabaseTest : virtual void SetUp() { scwx::qt::config::CountyDatabase::Initialize(); } }; +class CountyCountTest : + public testing::TestWithParam> +{ + virtual void SetUp() { scwx::qt::config::CountyDatabase::Initialize(); } +}; + TEST_P(CountyDatabaseTest, CountyName) { auto& [id, name] = GetParam(); @@ -24,6 +30,15 @@ TEST_P(CountyDatabaseTest, CountyName) EXPECT_EQ(actualName, name); } +TEST_P(CountyCountTest, State) +{ + auto& [state, size] = GetParam(); + + auto counties = CountyDatabase::GetCounties(state); + + EXPECT_EQ(counties.size(), size); +} + INSTANTIATE_TEST_SUITE_P( CountyDatabase, CountyDatabaseTest, @@ -33,6 +48,14 @@ INSTANTIATE_TEST_SUITE_P( std::make_pair("GMZ335", "Galveston Bay"), std::make_pair("ANZ338", "New York Harbor"))); +INSTANTIATE_TEST_SUITE_P(CountyDatabase, + CountyCountTest, + testing::Values(std::make_pair("AZ", 15), + std::make_pair("MO", 115), + std::make_pair("TX", 254), + std::make_pair("GM", 0), + std::make_pair("AN", 0))); + } // namespace config } // namespace qt } // namespace scwx From 769ce896e7eb83d9afb97cd0a6eb36930818f6eb Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 8 Dec 2023 05:15:57 -0600 Subject: [PATCH 23/29] Add county selection dialog --- scwx-qt/scwx-qt.cmake | 3 + .../source/scwx/qt/config/county_database.cpp | 5 + .../source/scwx/qt/config/county_database.hpp | 1 + scwx-qt/source/scwx/qt/ui/county_dialog.cpp | 175 ++++++++++++++++++ scwx-qt/source/scwx/qt/ui/county_dialog.hpp | 37 ++++ scwx-qt/source/scwx/qt/ui/county_dialog.ui | 104 +++++++++++ 6 files changed, 325 insertions(+) create mode 100644 scwx-qt/source/scwx/qt/ui/county_dialog.cpp create mode 100644 scwx-qt/source/scwx/qt/ui/county_dialog.hpp create mode 100644 scwx-qt/source/scwx/qt/ui/county_dialog.ui diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index b032971e..a5ab11f9 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -203,6 +203,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp source/scwx/qt/ui/alert_dock_widget.hpp source/scwx/qt/ui/animation_dock_widget.hpp source/scwx/qt/ui/collapsible_group.hpp + source/scwx/qt/ui/county_dialog.hpp source/scwx/qt/ui/flow_layout.hpp source/scwx/qt/ui/imgui_debug_dialog.hpp source/scwx/qt/ui/imgui_debug_widget.hpp @@ -222,6 +223,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp source/scwx/qt/ui/alert_dock_widget.cpp source/scwx/qt/ui/animation_dock_widget.cpp source/scwx/qt/ui/collapsible_group.cpp + source/scwx/qt/ui/county_dialog.cpp source/scwx/qt/ui/flow_layout.cpp source/scwx/qt/ui/imgui_debug_dialog.cpp source/scwx/qt/ui/imgui_debug_widget.cpp @@ -241,6 +243,7 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/alert_dock_widget.ui source/scwx/qt/ui/animation_dock_widget.ui source/scwx/qt/ui/collapsible_group.ui + source/scwx/qt/ui/county_dialog.ui source/scwx/qt/ui/imgui_debug_dialog.ui source/scwx/qt/ui/layer_dialog.ui source/scwx/qt/ui/open_url_dialog.ui diff --git a/scwx-qt/source/scwx/qt/config/county_database.cpp b/scwx-qt/source/scwx/qt/config/county_database.cpp index de968474..106b6911 100644 --- a/scwx-qt/source/scwx/qt/config/county_database.cpp +++ b/scwx-qt/source/scwx/qt/config/county_database.cpp @@ -225,6 +225,11 @@ GetCounties(const std::string& state) return counties; } +const std::unordered_map& GetStates() +{ + return stateMap_; +} + } // namespace CountyDatabase } // namespace config } // namespace qt diff --git a/scwx-qt/source/scwx/qt/config/county_database.hpp b/scwx-qt/source/scwx/qt/config/county_database.hpp index 5cc92d3d..5ee33e11 100644 --- a/scwx-qt/source/scwx/qt/config/county_database.hpp +++ b/scwx-qt/source/scwx/qt/config/county_database.hpp @@ -18,6 +18,7 @@ void Initialize(); std::string GetCountyName(const std::string& id); std::unordered_map GetCounties(const std::string& state); +const std::unordered_map& GetStates(); } // namespace CountyDatabase } // namespace config diff --git a/scwx-qt/source/scwx/qt/ui/county_dialog.cpp b/scwx-qt/source/scwx/qt/ui/county_dialog.cpp new file mode 100644 index 00000000..64c6d574 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/county_dialog.cpp @@ -0,0 +1,175 @@ +#include "county_dialog.hpp" +#include "ui_county_dialog.h" + +#include +#include + +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ + +static const std::string logPrefix_ = "scwx::qt::ui::county_dialog"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +class CountyDialog::Impl +{ +public: + explicit Impl(CountyDialog* self) : + self_ {self}, + model_ {new QStandardItemModel(self)}, + proxyModel_ {new QSortFilterProxyModel(self)}, + states_ {config::CountyDatabase::GetStates()} + { + } + ~Impl() = default; + + void UpdateModel(const std::string& stateName); + + CountyDialog* self_; + QStandardItemModel* model_; + QSortFilterProxyModel* proxyModel_; + + std::string selectedCounty_ {"?"}; + + const std::unordered_map& states_; +}; + +CountyDialog::CountyDialog(QWidget* parent) : + QDialog(parent), p {std::make_unique(this)}, ui(new Ui::CountyDialog) +{ + ui->setupUi(this); + + for (auto& state : p->states_) + { + ui->stateComboBox->addItem(QString::fromStdString(state.second)); + } + ui->stateComboBox->model()->sort(0); + ui->stateComboBox->setCurrentIndex(0); + + p->proxyModel_->setSourceModel(p->model_); + ui->countyView->setModel(p->proxyModel_); + ui->countyView->setEditTriggers( + QAbstractItemView::EditTrigger::NoEditTriggers); + ui->countyView->sortByColumn(0, Qt::SortOrder::AscendingOrder); + ui->countyView->header()->setSectionResizeMode( + QHeaderView::ResizeMode::Stretch); + + connect(ui->stateComboBox, + &QComboBox::currentTextChanged, + this, + [this](const QString& text) { p->UpdateModel(text.toStdString()); }); + p->UpdateModel(ui->stateComboBox->currentText().toStdString()); + + // Button Box + ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok) + ->setEnabled(false); + + connect(ui->countyView, + &QTreeView::doubleClicked, + this, + [this]() { Q_EMIT accept(); }); + connect( + ui->countyView->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + [this](const QItemSelection& selected, const QItemSelection& deselected) + { + if (selected.size() == 0 && deselected.size() == 0) + { + // Items which stay selected but change their index are not + // included in selected and deselected. Thus, this signal might + // be emitted with both selected and deselected empty, if only + // the indices of selected items change. + return; + } + + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(selected.size() > 0); + + if (selected.size() > 0) + { + QModelIndex selectedIndex = + p->proxyModel_->mapToSource(selected[0].indexes()[0]); + selectedIndex = p->model_->index(selectedIndex.row(), 1); + QVariant variantData = p->model_->data(selectedIndex); + if (variantData.typeId() == QMetaType::QString) + { + p->selectedCounty_ = variantData.toString().toStdString(); + } + else + { + logger_->warn("Unexpected selection data type"); + p->selectedCounty_ = std::string {"?"}; + } + } + else + { + p->selectedCounty_ = std::string {"?"}; + } + + logger_->debug("Selected: {}", p->selectedCounty_); + }); +} + +CountyDialog::~CountyDialog() +{ + delete ui; +} + +std::string CountyDialog::county_fips_id() +{ + return p->selectedCounty_; +} + +void CountyDialog::SelectState(const std::string& state) +{ + auto it = p->states_.find(state); + if (it != p->states_.cend()) + { + ui->stateComboBox->setCurrentText(QString::fromStdString(it->second)); + } +} + +void CountyDialog::Impl::UpdateModel(const std::string& stateName) +{ + // Clear existing counties + model_->clear(); + + // Reset selected county and disable OK button + selectedCounty_ = std::string {"?"}; + self_->ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok) + ->setEnabled(false); + + // Reset headers + model_->setHorizontalHeaderLabels({tr("County / Area"), tr("FIPS ID")}); + + // Find the state ID from the statename + auto it = std::find_if(states_.cbegin(), + states_.cend(), + [&](const std::pair& record) + { return record.second == stateName; }); + + if (it != states_.cend()) + { + QStandardItem* root = model_->invisibleRootItem(); + + // Add each county to the model + for (auto& county : config::CountyDatabase::GetCounties(it->first)) + { + root->appendRow( + {new QStandardItem(QString::fromStdString(county.second)), + new QStandardItem(QString::fromStdString(county.first))}); + } + } +} + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/county_dialog.hpp b/scwx-qt/source/scwx/qt/ui/county_dialog.hpp new file mode 100644 index 00000000..76045c81 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/county_dialog.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace Ui +{ +class CountyDialog; +} + +namespace scwx +{ +namespace qt +{ +namespace ui +{ +class CountyDialog : public QDialog +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(CountyDialog) + +public: + explicit CountyDialog(QWidget* parent = nullptr); + ~CountyDialog(); + + std::string county_fips_id(); + + void SelectState(const std::string& state); + +private: + class Impl; + std::unique_ptr p; + Ui::CountyDialog* ui; +}; + +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/county_dialog.ui b/scwx-qt/source/scwx/qt/ui/county_dialog.ui new file mode 100644 index 00000000..71741c86 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/county_dialog.ui @@ -0,0 +1,104 @@ + + + CountyDialog + + + + 0 + 0 + 400 + 400 + + + + Select County + + + + + + true + + + 0 + + + true + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + buttonBox + accepted() + CountyDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + CountyDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + From c970e73db85cd33ef039bc2223fcdd94065ce4c0 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 8 Dec 2023 05:59:22 -0600 Subject: [PATCH 24/29] Add alert county to settings --- .../scwx/qt/settings/audio_settings.cpp | 19 ++- .../scwx/qt/settings/audio_settings.hpp | 1 + .../source/scwx/qt/types/location_types.cpp | 1 + .../source/scwx/qt/types/location_types.hpp | 3 +- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 99 ++++++++++--- scwx-qt/source/scwx/qt/ui/settings_dialog.ui | 131 ++++++++++++------ test/data | 2 +- 7 files changed, 193 insertions(+), 63 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp index f5719525..c8bae71b 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -47,6 +48,14 @@ class AudioSettings::Impl types::LocationMethodIterator(), types::GetLocationMethodName)); + alertCounty_.SetValidator( + [](const std::string& value) + { + // Empty, or county exists in the database + return value.empty() || + config::CountyDatabase::GetCountyName(value) != value; + }); + for (auto& phenomenon : types::GetAlertAudioPhenomena()) { std::string phenomenonCode = awips::GetPhenomenonCode(phenomenon); @@ -73,6 +82,7 @@ class AudioSettings::Impl SettingsVariable alertLocationMethod_ {"alert_location_method"}; SettingsVariable alertLatitude_ {"alert_latitude"}; SettingsVariable alertLongitude_ {"alert_longitude"}; + SettingsVariable alertCounty_ {"alert_county"}; std::unordered_map> alertEnabled_ {}; @@ -85,7 +95,8 @@ AudioSettings::AudioSettings() : RegisterVariables({&p->alertSoundFile_, &p->alertLocationMethod_, &p->alertLatitude_, - &p->alertLongitude_}); + &p->alertLongitude_, + &p->alertCounty_}); RegisterVariables(p->variables_); SetDefaults(); @@ -116,6 +127,11 @@ SettingsVariable& AudioSettings::alert_longitude() const return p->alertLongitude_; } +SettingsVariable& AudioSettings::alert_county() const +{ + return p->alertCounty_; +} + SettingsVariable& AudioSettings::alert_enabled(awips::Phenomenon phenomenon) const { @@ -139,6 +155,7 @@ bool operator==(const AudioSettings& lhs, const AudioSettings& rhs) lhs.p->alertLocationMethod_ == rhs.p->alertLocationMethod_ && lhs.p->alertLatitude_ == rhs.p->alertLatitude_ && lhs.p->alertLongitude_ == rhs.p->alertLongitude_ && + lhs.p->alertCounty_ == rhs.p->alertCounty_ && lhs.p->alertEnabled_ == rhs.p->alertEnabled_); } diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp index e332cc8d..07deca1c 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp @@ -30,6 +30,7 @@ class AudioSettings : public SettingsCategory SettingsVariable& alert_location_method() const; SettingsVariable& alert_latitude() const; SettingsVariable& alert_longitude() const; + SettingsVariable& alert_county() const; SettingsVariable& alert_enabled(awips::Phenomenon phenomenon) const; static AudioSettings& Instance(); diff --git a/scwx-qt/source/scwx/qt/types/location_types.cpp b/scwx-qt/source/scwx/qt/types/location_types.cpp index 619e7746..4732eb2e 100644 --- a/scwx-qt/source/scwx/qt/types/location_types.cpp +++ b/scwx-qt/source/scwx/qt/types/location_types.cpp @@ -15,6 +15,7 @@ namespace types static const std::unordered_map locationMethodName_ {{LocationMethod::Fixed, "Fixed"}, {LocationMethod::Track, "Track"}, + {LocationMethod::County, "County"}, {LocationMethod::Unknown, "?"}}; SCWX_GET_ENUM(LocationMethod, GetLocationMethod, locationMethodName_) diff --git a/scwx-qt/source/scwx/qt/types/location_types.hpp b/scwx-qt/source/scwx/qt/types/location_types.hpp index 9a6cca05..c9d9784e 100644 --- a/scwx-qt/source/scwx/qt/types/location_types.hpp +++ b/scwx-qt/source/scwx/qt/types/location_types.hpp @@ -15,10 +15,11 @@ enum class LocationMethod { Fixed, Track, + County, Unknown }; typedef scwx::util:: - Iterator + Iterator LocationMethodIterator; LocationMethod GetLocationMethod(const std::string& name); diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 4f983a68..36bef8d5 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -18,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -113,6 +115,7 @@ class SettingsDialogImpl explicit SettingsDialogImpl(SettingsDialog* self) : self_ {self}, radarSiteDialog_ {new RadarSiteDialog(self)}, + countyDialog_ {new CountyDialog(self)}, fontDialog_ {new QFontDialog(self)}, fontCategoryModel_ {new QStandardItemModel(self)}, settings_ {std::initializer_list { @@ -131,6 +134,7 @@ class SettingsDialogImpl &alertAudioLocationMethod_, &alertAudioLatitude_, &alertAudioLongitude_, + &alertAudioCounty_, &hoverTextWrap_, &tooltipMethod_, &placefileTextDropShadowEnabled_}} @@ -192,6 +196,7 @@ class SettingsDialogImpl SettingsDialog* self_; RadarSiteDialog* radarSiteDialog_; + CountyDialog* countyDialog_; QFontDialog* fontDialog_; QStandardItemModel* fontCategoryModel_; @@ -228,6 +233,7 @@ class SettingsDialogImpl settings::SettingsInterface alertAudioLocationMethod_ {}; settings::SettingsInterface alertAudioLatitude_ {}; settings::SettingsInterface alertAudioLongitude_ {}; + settings::SettingsInterface alertAudioCounty_ {}; std::unordered_map> alertAudioEnabled_ {}; @@ -826,26 +832,34 @@ void SettingsDialogImpl::SetupPalettesAlertsTab() void SettingsDialogImpl::SetupAudioTab() { - QObject::connect(self_->ui->alertAudioLocationMethodComboBox, - &QComboBox::currentTextChanged, - self_, - [this](const QString& text) - { - types::LocationMethod locationMethod = - types::GetLocationMethod(text.toStdString()); - - bool coordinateEntryEnabled = - locationMethod == types::LocationMethod::Fixed; - - self_->ui->alertAudioLatitudeSpinBox->setEnabled( - coordinateEntryEnabled); - self_->ui->alertAudioLongitudeSpinBox->setEnabled( - coordinateEntryEnabled); - self_->ui->resetAlertAudioLatitudeButton->setEnabled( - coordinateEntryEnabled); - self_->ui->resetAlertAudioLongitudeButton->setEnabled( - coordinateEntryEnabled); - }); + QObject::connect( + self_->ui->alertAudioLocationMethodComboBox, + &QComboBox::currentTextChanged, + self_, + [this](const QString& text) + { + types::LocationMethod locationMethod = + types::GetLocationMethod(text.toStdString()); + + bool coordinateEntryEnabled = + locationMethod == types::LocationMethod::Fixed; + bool countyEntryEnabled = + locationMethod == types::LocationMethod::County; + + self_->ui->alertAudioLatitudeSpinBox->setEnabled( + coordinateEntryEnabled); + self_->ui->alertAudioLongitudeSpinBox->setEnabled( + coordinateEntryEnabled); + self_->ui->resetAlertAudioLatitudeButton->setEnabled( + coordinateEntryEnabled); + self_->ui->resetAlertAudioLongitudeButton->setEnabled( + coordinateEntryEnabled); + + self_->ui->alertAudioCountyLineEdit->setEnabled(countyEntryEnabled); + self_->ui->alertAudioCountySelectButton->setEnabled( + countyEntryEnabled); + self_->ui->resetAlertAudioCountyButton->setEnabled(countyEntryEnabled); + }); settings::AudioSettings& audioSettings = settings::AudioSettings::Instance(); @@ -927,6 +941,10 @@ void SettingsDialogImpl::SetupAudioTab() alertAudioLongitude_.SetResetButton( self_->ui->resetAlertAudioLongitudeButton); + alertAudioCounty_.SetSettingsVariable(audioSettings.alert_county()); + alertAudioCounty_.SetEditWidget(self_->ui->alertAudioCountyLineEdit); + alertAudioCounty_.SetResetButton(self_->ui->resetAlertAudioCountyButton); + auto alertAudioLayout = static_cast(self_->ui->alertAudioGroupBox->layout()); @@ -974,6 +992,47 @@ void SettingsDialogImpl::SetupAudioTab() coordinate.longitude()); } }); + + QObject::connect( + self_->ui->alertAudioCountySelectButton, + &QAbstractButton::clicked, + self_, + [this]() + { + std::string countyId = + self_->ui->alertAudioCountyLineEdit->text().toStdString(); + + if (countyId.length() >= 2) + { + countyDialog_->SelectState(countyId.substr(0, 2)); + } + + countyDialog_->show(); + }); + QObject::connect(countyDialog_, + &CountyDialog::accepted, + self_, + [this]() + { + std::string countyId = countyDialog_->county_fips_id(); + QString qCountyId = QString::fromStdString(countyId); + self_->ui->alertAudioCountyLineEdit->setText(qCountyId); + + // setText does not emit the textEdited signal + Q_EMIT self_->ui->alertAudioCountyLineEdit->textEdited( + qCountyId); + }); + QObject::connect(self_->ui->alertAudioCountyLineEdit, + &QLineEdit::textChanged, + self_, + [this](const QString& text) + { + std::string countyName = + config::CountyDatabase::GetCountyName( + text.toStdString()); + self_->ui->alertAudioCountyLabel->setText( + QString::fromStdString(countyName)); + }); } void SettingsDialogImpl::SetupTextTab() diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index 681923f5..7dfea913 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -460,22 +460,6 @@ - - - - 4 - - - -90.000000000000000 - - - 90.000000000000000 - - - 0.000100000000000 - - - @@ -508,22 +492,6 @@ - - - - 4 - - - -180.000000000000000 - - - 180.000000000000000 - - - 0.000100000000000 - - - @@ -535,6 +503,14 @@ + + + + + :/res/icons/font-awesome-6/stop-solid.svg:/res/icons/font-awesome-6/stop-solid.svg + + + @@ -546,9 +522,6 @@ - - - @@ -575,7 +548,46 @@ - + + + + County + + + + + + + 4 + + + -180.000000000000000 + + + 180.000000000000000 + + + 0.000100000000000 + + + + + + + 4 + + + -90.000000000000000 + + + 90.000000000000000 + + + 0.000100000000000 + + + + @@ -585,11 +597,50 @@ - - + + + + + + + ... + - :/res/icons/font-awesome-6/stop-solid.svg:/res/icons/font-awesome-6/stop-solid.svg + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + ... + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + true @@ -604,7 +655,7 @@ 20 - 281 + 253 diff --git a/test/data b/test/data index 8a633285..71d18bc6 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 8a633285ecbe8ff7d48631485620a7c7a5257229 +Subproject commit 71d18bc659d5e2b68b57dfaeb20f87cb1f0675f2 From bdb859480f1952051d9251a23343c57a3119ce19 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 8 Dec 2023 06:00:17 -0600 Subject: [PATCH 25/29] County database must be loaded before settings --- scwx-qt/source/scwx/qt/main/main.cpp | 2 ++ scwx-qt/source/scwx/qt/manager/resource_manager.cpp | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 82e9b152..8a52aab2 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -1,5 +1,6 @@ #define _SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING +#include #include #include #include @@ -72,6 +73,7 @@ int main(int argc, char* argv[]) // Initialize application scwx::qt::config::RadarSite::Initialize(); + scwx::qt::config::CountyDatabase::Initialize(); scwx::qt::manager::SettingsManager::Instance().Initialize(); // Theme diff --git a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp index df4b4f77..c9c40b6b 100644 --- a/scwx-qt/source/scwx/qt/manager/resource_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/resource_manager.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -33,8 +32,6 @@ static const std::vector> fontNames_ { void Initialize() { - config::CountyDatabase::Initialize(); - LoadFonts(); LoadTextures(); } From e2c8b3c7db6bf71b20b98910dadabaa58d5b88cc Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 8 Dec 2023 06:00:51 -0600 Subject: [PATCH 26/29] Handle alert audio when location method is county --- .../source/scwx/qt/manager/alert_manager.cpp | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/scwx-qt/source/scwx/qt/manager/alert_manager.cpp b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp index 3bbf1d31..dfaa8707 100644 --- a/scwx-qt/source/scwx/qt/manager/alert_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp @@ -48,7 +48,8 @@ class AlertManager::Impl ~Impl() { threadPool_.join(); } - common::Coordinate CurrentCoordinate() const; + common::Coordinate + CurrentCoordinate(types::LocationMethod locationMethod) const; void HandleAlert(const types::TextEventKey& key, size_t messageIndex) const; void UpdateLocationTracking(const std::string& value) const; @@ -68,14 +69,12 @@ class AlertManager::Impl AlertManager::AlertManager() : p(std::make_unique(this)) {} AlertManager::~AlertManager() = default; -common::Coordinate AlertManager::Impl::CurrentCoordinate() const +common::Coordinate AlertManager::Impl::CurrentCoordinate( + types::LocationMethod locationMethod) const { settings::AudioSettings& audioSettings = settings::AudioSettings::Instance(); common::Coordinate coordinate {}; - types::LocationMethod locationMethod = types::GetLocationMethod( - audioSettings.alert_location_method().GetValue()); - if (locationMethod == types::LocationMethod::Fixed) { coordinate.latitude_ = audioSettings.alert_latitude().GetValue(); @@ -105,7 +104,10 @@ void AlertManager::Impl::HandleAlert(const types::TextEventKey& key, } settings::AudioSettings& audioSettings = settings::AudioSettings::Instance(); - common::Coordinate currentCoordinate = CurrentCoordinate(); + types::LocationMethod locationMethod = types::GetLocationMethod( + audioSettings.alert_location_method().GetValue()); + common::Coordinate currentCoordinate = CurrentCoordinate(locationMethod); + std::string alertCounty = audioSettings.alert_county().GetValue(); auto message = textEventManager_->message_list(key).at(messageIndex); @@ -130,11 +132,27 @@ void AlertManager::Impl::HandleAlert(const types::TextEventKey& key, continue; } - // Determine if the alert is active at the current coordinte - auto alertCoordinates = segment->codedLocation_->coordinates(); + bool activeAtLocation = false; + + if (locationMethod == types::LocationMethod::Fixed || + locationMethod == types::LocationMethod::Track) + { + + // Determine if the alert is active at the current coordinte + auto alertCoordinates = segment->codedLocation_->coordinates(); + + activeAtLocation = util::GeographicLib::AreaContainsPoint( + alertCoordinates, currentCoordinate); + } + else if (locationMethod == types::LocationMethod::County) + { + // Determine if the alert contains the current county + auto fipsIds = segment->header_->ugc_.fips_ids(); + auto it = std::find(fipsIds.cbegin(), fipsIds.cend(), alertCounty); + activeAtLocation = it != fipsIds.cend(); + } - if (util::GeographicLib::AreaContainsPoint(alertCoordinates, - currentCoordinate)) + if (activeAtLocation) { logger_->info("Alert active at current location: {} {}.{} {}", vtec.pVtec_.office_id(), From d7606417705db44439fb1856a298eeb45240c085 Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 8 Dec 2023 06:31:57 -0600 Subject: [PATCH 27/29] Add ignore missing codecs to settings --- scwx-qt/source/scwx/qt/settings/audio_settings.cpp | 10 +++++++++- scwx-qt/source/scwx/qt/settings/audio_settings.hpp | 1 + test/data | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp index c8bae71b..2145daba 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp @@ -37,6 +37,7 @@ class AudioSettings::Impl alertLocationMethod_.SetDefault(defaultAlertLocationMethodValue); alertLatitude_.SetDefault(0.0); alertLongitude_.SetDefault(0.0); + ignoreMissingCodecs_.SetDefault(false); alertLatitude_.SetMinimum(-90.0); alertLatitude_.SetMaximum(90.0); @@ -83,6 +84,7 @@ class AudioSettings::Impl SettingsVariable alertLatitude_ {"alert_latitude"}; SettingsVariable alertLongitude_ {"alert_longitude"}; SettingsVariable alertCounty_ {"alert_county"}; + SettingsVariable ignoreMissingCodecs_ {"ignore_missing_codecs"}; std::unordered_map> alertEnabled_ {}; @@ -96,7 +98,8 @@ AudioSettings::AudioSettings() : &p->alertLocationMethod_, &p->alertLatitude_, &p->alertLongitude_, - &p->alertCounty_}); + &p->alertCounty_, + &p->ignoreMissingCodecs_}); RegisterVariables(p->variables_); SetDefaults(); @@ -143,6 +146,11 @@ AudioSettings::alert_enabled(awips::Phenomenon phenomenon) const return alert->second; } +SettingsVariable& AudioSettings::ignore_missing_codecs() const +{ + return p->ignoreMissingCodecs_; +} + AudioSettings& AudioSettings::Instance() { static AudioSettings audioSettings_; diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp index 07deca1c..19012e84 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp @@ -32,6 +32,7 @@ class AudioSettings : public SettingsCategory SettingsVariable& alert_longitude() const; SettingsVariable& alert_county() const; SettingsVariable& alert_enabled(awips::Phenomenon phenomenon) const; + SettingsVariable& ignore_missing_codecs() const; static AudioSettings& Instance(); diff --git a/test/data b/test/data index 71d18bc6..65bdc55e 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 71d18bc659d5e2b68b57dfaeb20f87cb1f0675f2 +Subproject commit 65bdc55e4afa29c24010a398238f2036060bbd0c From f143186ea557db643a3328871a0db17bbae60ccf Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 8 Dec 2023 09:56:01 -0600 Subject: [PATCH 28/29] Update setup wizard for missing media codecs --- scwx-qt/scwx-qt.cmake | 6 +- .../scwx/qt/ui/setup/audio_codec_page.cpp | 176 ++++++++++++++++++ .../scwx/qt/ui/setup/audio_codec_page.hpp | 34 ++++ .../scwx/qt/ui/setup/map_provider_page.cpp | 12 ++ .../scwx/qt/ui/setup/map_provider_page.hpp | 2 + .../source/scwx/qt/ui/setup/setup_wizard.cpp | 44 ++++- .../source/scwx/qt/ui/setup/setup_wizard.hpp | 3 + 7 files changed, 267 insertions(+), 10 deletions(-) create mode 100644 scwx-qt/source/scwx/qt/ui/setup/audio_codec_page.cpp create mode 100644 scwx-qt/source/scwx/qt/ui/setup/audio_codec_page.hpp diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index a5ab11f9..99ede29f 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -252,12 +252,14 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui source/scwx/qt/ui/radar_site_dialog.ui source/scwx/qt/ui/settings_dialog.ui source/scwx/qt/ui/update_dialog.ui) -set(HDR_UI_SETUP source/scwx/qt/ui/setup/finish_page.hpp +set(HDR_UI_SETUP source/scwx/qt/ui/setup/audio_codec_page.hpp + source/scwx/qt/ui/setup/finish_page.hpp source/scwx/qt/ui/setup/map_layout_page.hpp source/scwx/qt/ui/setup/map_provider_page.hpp source/scwx/qt/ui/setup/setup_wizard.hpp source/scwx/qt/ui/setup/welcome_page.hpp) -set(SRC_UI_SETUP source/scwx/qt/ui/setup/finish_page.cpp +set(SRC_UI_SETUP source/scwx/qt/ui/setup/audio_codec_page.cpp + source/scwx/qt/ui/setup/finish_page.cpp source/scwx/qt/ui/setup/map_layout_page.cpp source/scwx/qt/ui/setup/map_provider_page.cpp source/scwx/qt/ui/setup/setup_wizard.cpp diff --git a/scwx-qt/source/scwx/qt/ui/setup/audio_codec_page.cpp b/scwx-qt/source/scwx/qt/ui/setup/audio_codec_page.cpp new file mode 100644 index 00000000..2830ed3e --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/setup/audio_codec_page.cpp @@ -0,0 +1,176 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ +namespace setup +{ + +class AudioCodecPage::Impl +{ +public: + explicit Impl(AudioCodecPage* self) : self_ {self} {}; + ~Impl() = default; + + void SetupSettingsInterface(); + void SetInstructionsLabelText(); + + AudioCodecPage* self_; + + QLayout* layout_ {}; + QLayout* topLayout_ {}; + + QScrollArea* scrollArea_ {}; + QWidget* contents_ {}; + QLabel* descriptionLabel_ {}; + QLabel* instructionsLabel_ {}; + QCheckBox* ignoreMissingCodecsCheckBox_ {}; + QSpacerItem* spacer_ {}; + + settings::SettingsInterface ignoreMissingCodecs_ {}; +}; + +AudioCodecPage::AudioCodecPage(QWidget* parent) : + QWizardPage(parent), p {std::make_shared(this)} +{ + setTitle(tr("Media Codecs")); + setSubTitle(tr("Configure system media settings for Supercell Wx.")); + + p->descriptionLabel_ = new QLabel(this); + p->instructionsLabel_ = new QLabel(this); + p->ignoreMissingCodecsCheckBox_ = new QCheckBox(this); + + // Description + p->descriptionLabel_->setText( + tr("Your system does not have the proper codecs installed in order to " + "play the default audio. You may either install the proper codecs, or " + "update Supercell Wx audio settings to change from the default audio " + "files. After installing the proper codecs, you must restart " + "Supercell Wx.")); + p->descriptionLabel_->setWordWrap(true); + p->SetInstructionsLabelText(); + p->instructionsLabel_->setWordWrap(true); + + p->ignoreMissingCodecsCheckBox_->setText(tr("Ignore missing codecs")); + + p->spacer_ = + new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding); + + // Overall layout + p->layout_ = new QVBoxLayout(this); + p->layout_->addWidget(p->descriptionLabel_); + p->layout_->addWidget(p->instructionsLabel_); + p->layout_->addWidget(p->ignoreMissingCodecsCheckBox_); + p->layout_->addItem(p->spacer_); + + p->contents_ = new QWidget(this); + p->contents_->setLayout(p->layout_); + + p->scrollArea_ = new QScrollArea(this); + p->scrollArea_->setHorizontalScrollBarPolicy( + Qt::ScrollBarPolicy::ScrollBarAlwaysOff); + p->scrollArea_->setFrameShape(QFrame::Shape::NoFrame); + p->scrollArea_->setWidgetResizable(true); + p->scrollArea_->setWidget(p->contents_); + + p->topLayout_ = new QVBoxLayout(this); + p->topLayout_->setContentsMargins(0, 0, 0, 0); + p->topLayout_->addWidget(p->scrollArea_); + + setLayout(p->topLayout_); + + // Configure settings interface + p->SetupSettingsInterface(); +} + +AudioCodecPage::~AudioCodecPage() = default; + +void AudioCodecPage::Impl::SetInstructionsLabelText() +{ +#if defined(_WIN32) + instructionsLabel_->setText(tr( + "

Option 1

" // + "

Update your Windows installation. The required media codecs may " + "be available with the latest operating system updates.

" // + "

Option 2

" // + "

Install the Web " + "Media Extensions package from the Windows Store.

" // + "

Option 3

" // + "

Install K-Lite Codec Pack " + "Basic. This is a 3rd party application, and no support or warranty " + "is provided.

")); + instructionsLabel_->setTextInteractionFlags( + Qt::TextInteractionFlag::TextBrowserInteraction); + + QObject::connect(instructionsLabel_, + &QLabel::linkActivated, + self_, + [](const QString& link) + { QDesktopServices::openUrl(QUrl {link}); }); +#else + instructionsLabel_->setText( + tr("Please see the instructions for your Linux distribution for " + "installing media codecs.")); +#endif +} + +void AudioCodecPage::Impl::SetupSettingsInterface() +{ + auto& audioSettings = settings::AudioSettings::Instance(); + + ignoreMissingCodecs_.SetSettingsVariable( + audioSettings.ignore_missing_codecs()); + ignoreMissingCodecs_.SetEditWidget(ignoreMissingCodecsCheckBox_); +} + +bool AudioCodecPage::validatePage() +{ + bool committed = false; + + committed |= p->ignoreMissingCodecs_.Commit(); + + if (committed) + { + manager::SettingsManager::Instance().SaveSettings(); + } + + return true; +} + +bool AudioCodecPage::IsRequired() +{ + auto& audioSettings = settings::AudioSettings::Instance(); + + bool ignoreCodecErrors = audioSettings.ignore_missing_codecs().GetValue(); + + QMediaFormat oggFormat {QMediaFormat::FileFormat::Ogg}; + auto oggCodecs = + oggFormat.supportedAudioCodecs(QMediaFormat::ConversionMode::Decode); + + // Setup is required if codec errors are not ignored, and the default codecs + // are not supported + return (!ignoreCodecErrors && + oggCodecs.contains(QMediaFormat::AudioCodec::Vorbis)); +} + +} // namespace setup +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/setup/audio_codec_page.hpp b/scwx-qt/source/scwx/qt/ui/setup/audio_codec_page.hpp new file mode 100644 index 00000000..94a400e2 --- /dev/null +++ b/scwx-qt/source/scwx/qt/ui/setup/audio_codec_page.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +namespace scwx +{ +namespace qt +{ +namespace ui +{ +namespace setup +{ + +class AudioCodecPage : public QWizardPage +{ + Q_DISABLE_COPY_MOVE(AudioCodecPage) + +public: + explicit AudioCodecPage(QWidget* parent = nullptr); + ~AudioCodecPage(); + + bool validatePage() override; + + static bool IsRequired(); + +private: + class Impl; + std::shared_ptr p; +}; + +} // namespace setup +} // namespace ui +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.cpp b/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.cpp index 6cecf076..fc0c9ef1 100644 --- a/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.cpp +++ b/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.cpp @@ -278,6 +278,18 @@ bool MapProviderPage::validatePage() return true; } +bool MapProviderPage::IsRequired() +{ + auto& generalSettings = settings::GeneralSettings::Instance(); + + std::string mapboxApiKey = generalSettings.mapbox_api_key().GetValue(); + std::string maptilerApiKey = generalSettings.maptiler_api_key().GetValue(); + + // Setup is required if either API key is empty, or contains a single + // character ("?") + return (mapboxApiKey.size() <= 1 && maptilerApiKey.size() <= 1); +} + } // namespace setup } // namespace ui } // namespace qt diff --git a/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.hpp b/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.hpp index 5b7260c7..3e564639 100644 --- a/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.hpp +++ b/scwx-qt/source/scwx/qt/ui/setup/map_provider_page.hpp @@ -22,6 +22,8 @@ class MapProviderPage : public QWizardPage bool isComplete() const override; bool validatePage() override; + static bool IsRequired(); + private: class Impl; std::shared_ptr p; diff --git a/scwx-qt/source/scwx/qt/ui/setup/setup_wizard.cpp b/scwx-qt/source/scwx/qt/ui/setup/setup_wizard.cpp index 9daf903e..92be250f 100644 --- a/scwx-qt/source/scwx/qt/ui/setup/setup_wizard.cpp +++ b/scwx-qt/source/scwx/qt/ui/setup/setup_wizard.cpp @@ -1,9 +1,9 @@ #include +#include #include #include #include #include -#include #include #include @@ -38,6 +38,7 @@ SetupWizard::SetupWizard(QWidget* parent) : setPage(static_cast(Page::Welcome), new WelcomePage(this)); setPage(static_cast(Page::MapProvider), new MapProviderPage(this)); setPage(static_cast(Page::MapLayout), new MapLayoutPage(this)); + setPage(static_cast(Page::AudioCodec), new AudioCodecPage(this)); setPage(static_cast(Page::Finish), new FinishPage(this)); #if !defined(Q_OS_MAC) @@ -55,16 +56,43 @@ SetupWizard::SetupWizard(QWidget* parent) : SetupWizard::~SetupWizard() = default; -bool SetupWizard::IsSetupRequired() +int SetupWizard::nextId() const { - auto& generalSettings = settings::GeneralSettings::Instance(); + int nextId = currentId(); + + while (true) + { + switch (++nextId) + { + case static_cast(Page::MapProvider): + case static_cast(Page::MapLayout): + if (MapProviderPage::IsRequired()) + { + return nextId; + } + break; + + case static_cast(Page::AudioCodec): + if (AudioCodecPage::IsRequired()) + { + return nextId; + } + break; + + case static_cast(Page::Finish): + return nextId; - std::string mapboxApiKey = generalSettings.mapbox_api_key().GetValue(); - std::string maptilerApiKey = generalSettings.maptiler_api_key().GetValue(); + default: + return -1; + } + } - // Setup is required if either API key is empty, or contains a single - // character ("?") - return (mapboxApiKey.size() <= 1 && maptilerApiKey.size() <= 1); + return -1; +} + +bool SetupWizard::IsSetupRequired() +{ + return (MapProviderPage::IsRequired() || AudioCodecPage::IsRequired()); } } // namespace setup diff --git a/scwx-qt/source/scwx/qt/ui/setup/setup_wizard.hpp b/scwx-qt/source/scwx/qt/ui/setup/setup_wizard.hpp index d565c45a..c066cba9 100644 --- a/scwx-qt/source/scwx/qt/ui/setup/setup_wizard.hpp +++ b/scwx-qt/source/scwx/qt/ui/setup/setup_wizard.hpp @@ -19,12 +19,15 @@ class SetupWizard : public QWizard Welcome = 0, MapProvider, MapLayout, + AudioCodec, Finish }; explicit SetupWizard(QWidget* parent = nullptr); ~SetupWizard(); + int nextId() const override; + static bool IsSetupRequired(); private: From 2f62319958172679afe4cbb971a9eda7d6cf20fc Mon Sep 17 00:00:00 2001 From: Dan Paulat Date: Fri, 8 Dec 2023 10:04:11 -0600 Subject: [PATCH 29/29] Reorder alert county settings initialization to ensure county name displays on startup --- scwx-qt/source/scwx/qt/ui/settings_dialog.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index 36bef8d5..edd63ec8 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -941,10 +941,6 @@ void SettingsDialogImpl::SetupAudioTab() alertAudioLongitude_.SetResetButton( self_->ui->resetAlertAudioLongitudeButton); - alertAudioCounty_.SetSettingsVariable(audioSettings.alert_county()); - alertAudioCounty_.SetEditWidget(self_->ui->alertAudioCountyLineEdit); - alertAudioCounty_.SetResetButton(self_->ui->resetAlertAudioCountyButton); - auto alertAudioLayout = static_cast(self_->ui->alertAudioGroupBox->layout()); @@ -1033,6 +1029,10 @@ void SettingsDialogImpl::SetupAudioTab() self_->ui->alertAudioCountyLabel->setText( QString::fromStdString(countyName)); }); + + alertAudioCounty_.SetSettingsVariable(audioSettings.alert_county()); + alertAudioCounty_.SetEditWidget(self_->ui->alertAudioCountyLineEdit); + alertAudioCounty_.SetResetButton(self_->ui->resetAlertAudioCountyButton); } void SettingsDialogImpl::SetupTextTab()