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/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 4e6fffa0..27f6686e 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) | @@ -33,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 | @@ -59,6 +60,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/conanfile.py b/conanfile.py index f23cb9cb..8c767b96 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/data b/data index 9b6c72f8..db52049e 160000 --- a/data +++ b/data @@ -1 +1 @@ -Subproject commit 9b6c72f847193bc29d3ff183b206f26a9b5c007e +Subproject commit db52049ea651fea92b06e5024cbff3a3d3d26bc8 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 00000000..d3c86e3c Binary files /dev/null and b/scwx-qt/res/audio/wikimedia/Emergency_Alert_System_Attention_Signal_20s.ogg differ 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/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.cmake b/scwx-qt/scwx-qt.cmake index 809306e9..99ede29f 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) @@ -22,6 +23,7 @@ find_package(SQLite3) find_package(QT NAMES Qt6 COMPONENTS Gui LinguistTools + Multimedia Network OpenGL OpenGLWidgets @@ -31,6 +33,7 @@ find_package(QT NAMES Qt6 find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Gui LinguistTools + Multimedia Network OpenGL OpenGLWidgets @@ -76,7 +79,9 @@ 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 source/scwx/qt/manager/radar_product_manager.hpp @@ -86,7 +91,9 @@ 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 source/scwx/qt/manager/radar_product_manager.cpp @@ -141,18 +148,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 @@ -168,7 +178,9 @@ 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 source/scwx/qt/types/radar_product_record.hpp source/scwx/qt/types/text_event_key.hpp @@ -178,7 +190,9 @@ 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 source/scwx/qt/types/radar_product_record.cpp source/scwx/qt/types/text_event_key.cpp @@ -189,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 @@ -208,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 @@ -227,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 @@ -235,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 @@ -309,6 +328,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) @@ -397,8 +417,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}) @@ -512,6 +536,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 @@ -519,6 +544,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/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index 3e7e55dd..53d7aec2 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 @@ -43,6 +44,8 @@ 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 res/palettes/wct/DOD_DSD.pal diff --git a/scwx-qt/source/scwx/qt/config/county_database.cpp b/scwx-qt/source/scwx/qt/config/county_database.cpp index a0d7b51c..106b6911 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,13 @@ 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() { @@ -87,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( @@ -101,14 +104,24 @@ 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 if (columns != 2) + { + logger_->error( + "County database format error, invalid number of columns: {}", + columns); + status = -1; } else { - logger_->error( - "Database format error, invalid number of columns: {}", columns); + logger_->error("Invalid FIPS ID: {}", columnText[0]); status = -1; } @@ -122,20 +135,48 @@ void Initialize() sqlite3_free(errorMessage); } - // Finished populating county map, release lock - lock.unlock(); + // 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; + } + + return status; + }, + nullptr, + &errorMessage); + if (rc != SQLITE_OK) + { + logger_->error("SQL error: {}", errorMessage); + sqlite3_free(errorMessage); + } // Close database 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; @@ -143,17 +184,52 @@ 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; +} + +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 e75431ac..5ee33e11 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,9 @@ namespace CountyDatabase 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/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/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 @@ public: 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 @@ public: 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..dfaa8707 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp @@ -0,0 +1,197 @@ +#include +#include +#include +#include +#include +#include +#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); }); + + 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() { threadPool_.join(); } + + common::Coordinate + CurrentCoordinate(types::LocationMethod locationMethod) 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( + types::LocationMethod locationMethod) const +{ + settings::AudioSettings& audioSettings = settings::AudioSettings::Instance(); + common::Coordinate coordinate {}; + + 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(); + 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); + + 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; + } + + 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 (activeAtLocation) + { + 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(audioSettings.alert_sound_file().GetValue()); + } + } +} + +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 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..349e73b9 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/media_manager.cpp @@ -0,0 +1,131 @@ +#include +#include + +#include +#include +#include +#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(MediaManager* self) : + self_ {self}, + mediaDevices_ {new QMediaDevices(self)}, + mediaPlayer_ {new QMediaPlayer(self)}, + audioOutput_ {new QAudioOutput(self)} + { + logger_->debug("Audio device: {}", + audioOutput_->device().description().toStdString()); + + mediaPlayer_->setAudioOutput(audioOutput_); + + ConnectSignals(); + } + + ~Impl() {} + + void ConnectSignals(); + + MediaManager* self_; + + QMediaDevices* mediaDevices_; + QMediaPlayer* mediaPlayer_; + QAudioOutput* audioOutput_; +}; + +MediaManager::MediaManager() : p(std::make_unique(this)) {} +MediaManager::~MediaManager() = default; + +void MediaManager::Impl::ConnectSignals() +{ + QObject::connect( + mediaDevices_, + &QMediaDevices::audioOutputsChanged, + self_, + [this]() + { audioOutput_->setDevice(QMediaDevices::defaultAudioOutput()); }); + + 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); +} + +void MediaManager::Play(const std::string& mediaPath) +{ + logger_->debug("Playing audio: {}", mediaPath); + + 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_->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_ {}; + 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..f1d73656 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/media_manager.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +class MediaManager : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(MediaManager) + +public: + explicit MediaManager(); + ~MediaManager(); + + void Play(types::AudioFile media); + void Play(const std::string& mediaPath); + void Stop(); + + static std::shared_ptr Instance(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace manager +} // namespace qt +} // namespace scwx 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(); } 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..2145daba --- /dev/null +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp @@ -0,0 +1,172 @@ +#include +#include +#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::Unknown}; + +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); + ignoreMissingCodecs_.SetDefault(false); + + 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)); + + 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); + 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); + } + + // Create a default disabled alert, not stored in the settings file + alertEnabled_.emplace(kDefaultPhenomenon_, + SettingsVariable {"alert_disabled"}); + } + + ~Impl() {} + + SettingsVariable alertSoundFile_ {"alert_sound_file"}; + SettingsVariable alertLocationMethod_ {"alert_location_method"}; + SettingsVariable alertLatitude_ {"alert_latitude"}; + SettingsVariable alertLongitude_ {"alert_longitude"}; + SettingsVariable alertCounty_ {"alert_county"}; + SettingsVariable ignoreMissingCodecs_ {"ignore_missing_codecs"}; + + std::unordered_map> + alertEnabled_ {}; + std::vector variables_ {}; +}; + +AudioSettings::AudioSettings() : + SettingsCategory("audio"), p(std::make_unique()) +{ + RegisterVariables({&p->alertSoundFile_, + &p->alertLocationMethod_, + &p->alertLatitude_, + &p->alertLongitude_, + &p->alertCounty_, + &p->ignoreMissingCodecs_}); + RegisterVariables(p->variables_); + SetDefaults(); + + p->variables_.clear(); +} +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_; +} + +SettingsVariable& AudioSettings::alert_latitude() const +{ + return p->alertLatitude_; +} + +SettingsVariable& AudioSettings::alert_longitude() const +{ + return p->alertLongitude_; +} + +SettingsVariable& AudioSettings::alert_county() const +{ + return p->alertCounty_; +} + +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; +} + +SettingsVariable& AudioSettings::ignore_missing_codecs() const +{ + return p->ignoreMissingCodecs_; +} + +AudioSettings& AudioSettings::Instance() +{ + static AudioSettings audioSettings_; + return audioSettings_; +} + +bool operator==(const AudioSettings& lhs, const AudioSettings& rhs) +{ + 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->alertCounty_ == rhs.p->alertCounty_ && + 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..19012e84 --- /dev/null +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp @@ -0,0 +1,48 @@ +#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_sound_file() const; + SettingsVariable& alert_location_method() const; + SettingsVariable& alert_latitude() const; + SettingsVariable& alert_longitude() const; + SettingsVariable& alert_county() const; + SettingsVariable& alert_enabled(awips::Phenomenon phenomenon) const; + SettingsVariable& ignore_missing_codecs() 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/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/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..4732eb2e --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/location_types.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include + +#include + +namespace scwx +{ +namespace qt +{ +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_) + +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..c9d9784e --- /dev/null +++ b/scwx-qt/source/scwx/qt/types/location_types.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include + +namespace scwx +{ +namespace qt +{ +namespace types +{ + +enum class LocationMethod +{ + Fixed, + Track, + County, + 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/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..9e3c13ca --- /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, + ":/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 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 + + + + + diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index efc267bf..edd63ec8 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -3,17 +3,23 @@ #include #include +#include #include +#include +#include #include #include +#include #include #include #include #include #include #include +#include #include #include +#include #include #include #include @@ -26,6 +32,7 @@ #include #include #include +#include #include #include @@ -84,12 +91,31 @@ 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: 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 { @@ -104,6 +130,11 @@ public: &antiAliasingEnabled_, &updateNotificationsEnabled_, &debugEnabled_, + &alertAudioSoundFile_, + &alertAudioLocationMethod_, + &alertAudioLatitude_, + &alertAudioLongitude_, + &alertAudioCounty_, &hoverTextWrap_, &tooltipMethod_, &placefileTextDropShadowEnabled_}} @@ -136,6 +167,7 @@ public: void SetupGeneralTab(); void SetupPalettesColorTablesTab(); void SetupPalettesAlertsTab(); + void SetupAudioTab(); void SetupTextTab(); void ShowColorDialog(QLineEdit* lineEdit, QFrame* frame = nullptr); @@ -164,12 +196,18 @@ public: SettingsDialog* self_; RadarSiteDialog* radarSiteDialog_; + CountyDialog* countyDialog_; QFontDialog* fontDialog_; QStandardItemModel* fontCategoryModel_; types::FontCategory selectedFontCategory_ {types::FontCategory::Unknown}; + std::shared_ptr mediaManager_ { + manager::MediaManager::Instance()}; + std::shared_ptr positionManager_ { + manager::PositionManager::Instance()}; + settings::SettingsInterface defaultRadarSite_ {}; settings::SettingsInterface gridWidth_ {}; settings::SettingsInterface gridHeight_ {}; @@ -191,6 +229,15 @@ public: settings::SettingsInterface> inactiveAlertColors_ {}; + settings::SettingsInterface alertAudioSoundFile_ {}; + settings::SettingsInterface alertAudioLocationMethod_ {}; + settings::SettingsInterface alertAudioLatitude_ {}; + settings::SettingsInterface alertAudioLongitude_ {}; + settings::SettingsInterface alertAudioCounty_ {}; + + std::unordered_map> + alertAudioEnabled_ {}; + std::unordered_map> fontFamilies_ {}; @@ -223,6 +270,9 @@ SettingsDialog::SettingsDialog(QWidget* parent) : // Palettes > Alerts p->SetupPalettesAlertsTab(); + // Audio + p->SetupAudioTab(); + // Text p->SetupTextTab(); @@ -270,6 +320,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, @@ -766,6 +830,211 @@ 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; + 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(); + + alertAudioSoundFile_.SetSettingsVariable(audioSettings.alert_sound_file()); + 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( + 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); + } + + 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()); + } + }); + + 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)); + }); + + alertAudioCounty_.SetSettingsVariable(audioSettings.alert_county()); + alertAudioCounty_.SetEditWidget(self_->ui->alertAudioCountyLineEdit); + alertAudioCounty_.SetResetButton(self_->ui->resetAlertAudioCountyButton); +} + 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..7dfea913 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,223 @@ + + + + + + Alerts + + + + + + Latitude + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + Longitude + + + + + + + ... + + + + + + + Location Method + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + + :/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 + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + + :/res/icons/font-awesome-6/play-solid.svg:/res/icons/font-awesome-6/play-solid.svg + + + + + + + Sound + + + + + + + County + + + + + + + 4 + + + -180.000000000000000 + + + 180.000000000000000 + + + 0.000100000000000 + + + + + + + 4 + + + -90.000000000000000 + + + 90.000000000000000 + + + 0.000100000000000 + + + + + + + + 0 + 0 + + + + + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + ... + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 253 + + + + + + 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 @@ public: 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; +int SetupWizard::nextId() const +{ + 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; + + default: + return -1; + } + } + + return -1; +} + bool SetupWizard::IsSetupRequired() { - 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); + 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 @@ public: Welcome = 0, MapProvider, MapLayout, + AudioCodec, Finish }; explicit SetupWizard(QWidget* parent = nullptr); ~SetupWizard(); + int nextId() const override; + static bool IsSetupRequired(); private: diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp index 6718715a..8582d21c 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. * 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 @@ for countyDb in args.inputCountyDbs_: for zoneDb in args.inputZoneDbs_: ProcessZoneDbf(dbInfo, zoneDb) +for stateDb in args.inputStateDbs_: + ProcessStateDbf(dbInfo, stateDb) + PostProcess(dbInfo) diff --git a/test/data b/test/data index cd36a74a..65bdc55e 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit cd36a74a9c678d90d10ec397eae65b389a9640fc +Subproject commit 65bdc55e4afa29c24010a398238f2036060bbd0c 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 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