Merge pull request #103 from dpaulat/feature/alert-tones

Alert Tones
This commit is contained in:
Dan Paulat 2023-12-08 12:13:55 -06:00 committed by GitHub
commit 7b3d78e01a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 2240 additions and 52 deletions

View file

@ -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

View file

@ -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<br/>Additional Licenses: https://doc.qt.io/qt-6/licenses-used-in-qt.html |
| [Qt](https://www.qt.io/) | [GNU Lesser General Public License v3.0 only](https://spdx.org/licenses/LGPL-3.0-only.html) | Qt Core, Qt GUI, Qt Multimedia, Qt Network, Qt OpenGL, Qt Positioning, Qt SQL, Qt SVG, Qt Widgets<br/>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 |

View file

@ -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}

2
data

@ -1 +1 @@
Subproject commit 9b6c72f847193bc29d3ff183b206f26a9b5c007e
Subproject commit db52049ea651fea92b06e5024cbff3a3d3d26bc8

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="12" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="#000000" d="M0 128C0 92.7 28.7 64 64 64H320c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128z"/></svg>

After

Width:  |  Height:  |  Size: 388 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M533.6 32.5C598.5 85.3 640 165.8 640 256s-41.5 170.8-106.4 223.5c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C557.5 398.2 592 331.2 592 256s-34.5-142.2-88.7-186.3c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zM473.1 107c43.2 35.2 70.9 88.9 70.9 149s-27.7 113.8-70.9 149c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C475.3 341.3 496 301.1 496 256s-20.7-85.3-53.2-111.8c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zm-60.5 74.5C434.1 199.1 448 225.9 448 256s-13.9 56.9-35.4 74.5c-10.3 8.4-25.4 6.8-33.8-3.5s-6.8-25.4 3.5-33.8C393.1 284.4 400 271 400 256s-6.9-28.4-17.7-37.3c-10.3-8.4-11.8-23.5-3.5-33.8s23.5-11.8 33.8-3.5zM301.1 34.8C312.6 40 320 51.4 320 64V448c0 12.6-7.4 24-18.9 29.2s-25 3.1-34.4-5.3L131.8 352H64c-35.3 0-64-28.7-64-64V224c0-35.3 28.7-64 64-64h67.8L266.7 40.1c9.4-8.4 22.9-10.4 34.4-5.3z"/></svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -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
$<$<CXX_COMPILER_ID:MSVC>:opengl32>
Fontconfig::Fontconfig
GeographicLib::GeographicLib
GEOS::geos
GEOS::geos_cxx_flags
GLEW::GLEW
glm::glm
imgui

View file

@ -12,6 +12,7 @@
<file>gl/texture2d.frag</file>
<file>gl/texture2d_array.frag</file>
<file>gl/threshold.geom</file>
<file>res/audio/wikimedia/Emergency_Alert_System_Attention_Signal_20s.ogg</file>
<file>res/config/radar_sites.json</file>
<file>res/fonts/din1451alt.ttf</file>
<file>res/fonts/din1451alt_g.ttf</file>
@ -43,6 +44,8 @@
<file>res/icons/font-awesome-6/square-caret-right-regular.svg</file>
<file>res/icons/font-awesome-6/square-minus-regular.svg</file>
<file>res/icons/font-awesome-6/square-plus-regular.svg</file>
<file>res/icons/font-awesome-6/stop-solid.svg</file>
<file>res/icons/font-awesome-6/volume-high-solid.svg</file>
<file>res/palettes/wct/CC.pal</file>
<file>res/palettes/wct/Default16.pal</file>
<file>res/palettes/wct/DOD_DSD.pal</file>

View file

@ -1,7 +1,6 @@
#include <scwx/qt/config/county_database.hpp>
#include <scwx/util/logger.hpp>
#include <shared_mutex>
#include <unordered_map>
#include <boost/uuid/uuid.hpp>
@ -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<std::string, std::string> CountyMap;
typedef std::unordered_map<std::string, CountyMap> StateMap;
typedef std::unordered_map<char, StateMap> FormatMap;
static bool initialized_ {false};
static std::unordered_map<std::string, std::string> countyMap_;
static std::shared_mutex countyMutex_;
static FormatMap countyDatabase_;
static std::unordered_map<std::string, std::string> 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<std::string, std::string>
GetCounties(const std::string& state)
{
std::unordered_map<std::string, std::string> counties {};
StateMap& states = countyDatabase_.at('C');
auto it = states.find(state);
if (it != states.cend())
{
counties = it->second;
}
return counties;
}
const std::unordered_map<std::string, std::string>& GetStates()
{
return stateMap_;
}
} // namespace CountyDatabase
} // namespace config
} // namespace qt

View file

@ -2,6 +2,7 @@
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace scwx
@ -15,6 +16,9 @@ namespace CountyDatabase
void Initialize();
std::string GetCountyName(const std::string& id);
std::unordered_map<std::string, std::string>
GetCounties(const std::string& state);
const std::unordered_map<std::string, std::string>& GetStates();
} // namespace CountyDatabase
} // namespace config

View file

@ -1,5 +1,6 @@
#define _SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
#include <scwx/qt/config/county_database.hpp>
#include <scwx/qt/config/radar_site.hpp>
#include <scwx/qt/main/main_window.hpp>
#include <scwx/qt/main/versions.hpp>
@ -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

View file

@ -3,6 +3,7 @@
#include <scwx/qt/main/application.hpp>
#include <scwx/qt/main/versions.hpp>
#include <scwx/qt/manager/alert_manager.hpp>
#include <scwx/qt/manager/placefile_manager.hpp>
#include <scwx/qt/manager/position_manager.hpp>
#include <scwx/qt/manager/radar_product_manager.hpp>
@ -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<manager::AlertManager> alertManager_;
std::shared_ptr<manager::PlacefileManager> placefileManager_;
std::shared_ptr<manager::PositionManager> positionManager_;
std::shared_ptr<manager::TextEventManager> textEventManager_;

View file

@ -0,0 +1,197 @@
#include <scwx/qt/manager/alert_manager.hpp>
#include <scwx/qt/manager/media_manager.hpp>
#include <scwx/qt/manager/position_manager.hpp>
#include <scwx/qt/manager/text_event_manager.hpp>
#include <scwx/qt/settings/audio_settings.hpp>
#include <scwx/qt/types/location_types.hpp>
#include <scwx/qt/util/geographic_lib.hpp>
#include <scwx/util/logger.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/thread_pool.hpp>
#include <boost/uuid/random_generator.hpp>
#include <QGeoPositionInfo>
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_ {MediaManager::Instance()};
std::shared_ptr<PositionManager> positionManager_ {
PositionManager::Instance()};
std::shared_ptr<TextEventManager> textEventManager_ {
TextEventManager::Instance()};
};
AlertManager::AlertManager() : p(std::make_unique<Impl>(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> AlertManager::Instance()
{
static std::weak_ptr<AlertManager> alertManagerReference_ {};
static std::mutex instanceMutex_ {};
std::unique_lock lock(instanceMutex_);
std::shared_ptr<AlertManager> alertManager = alertManagerReference_.lock();
if (alertManager == nullptr)
{
alertManager = std::make_shared<AlertManager>();
alertManagerReference_ = alertManager;
}
return alertManager;
}
} // namespace manager
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,32 @@
#pragma once
#include <memory>
#include <QObject>
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<AlertManager> Instance();
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace manager
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,131 @@
#include <scwx/qt/manager/media_manager.hpp>
#include <scwx/util/logger.hpp>
#include <QAudioDevice>
#include <QAudioOutput>
#include <QMediaDevices>
#include <QMediaPlayer>
#include <QUrl>
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<Impl>(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<int>(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> MediaManager::Instance()
{
static std::weak_ptr<MediaManager> mediaManagerReference_ {};
static std::mutex instanceMutex_ {};
std::unique_lock lock(instanceMutex_);
std::shared_ptr<MediaManager> mediaManager = mediaManagerReference_.lock();
if (mediaManager == nullptr)
{
mediaManager = std::make_shared<MediaManager>();
mediaManagerReference_ = mediaManager;
}
return mediaManager;
}
} // namespace manager
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,38 @@
#pragma once
#include <scwx/qt/types/media_types.hpp>
#include <memory>
#include <QObject>
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<MediaManager> Instance();
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace manager
} // namespace qt
} // namespace scwx

View file

@ -1,6 +1,5 @@
#include <scwx/qt/manager/resource_manager.hpp>
#include <scwx/qt/manager/font_manager.hpp>
#include <scwx/qt/config/county_database.hpp>
#include <scwx/qt/model/imgui_context_model.hpp>
#include <scwx/qt/types/texture_types.hpp>
#include <scwx/qt/util/texture_atlas.hpp>
@ -33,8 +32,6 @@ static const std::vector<std::pair<types::Font, std::string>> fontNames_ {
void Initialize()
{
config::CountyDatabase::Initialize();
LoadFonts();
LoadTextures();
}

View file

@ -1,5 +1,6 @@
#include <scwx/qt/manager/settings_manager.hpp>
#include <scwx/qt/map/map_provider.hpp>
#include <scwx/qt/settings/audio_settings.hpp>
#include <scwx/qt/settings/general_settings.hpp>
#include <scwx/qt/settings/map_settings.hpp>
#include <scwx/qt/settings/palette_settings.hpp>
@ -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);

View file

@ -0,0 +1,172 @@
#include <scwx/qt/config/county_database.hpp>
#include <scwx/qt/settings/audio_settings.hpp>
#include <scwx/qt/settings/settings_definitions.hpp>
#include <scwx/qt/settings/settings_variable.hpp>
#include <scwx/qt/types/alert_types.hpp>
#include <scwx/qt/types/location_types.hpp>
#include <scwx/qt/types/media_types.hpp>
#include <boost/algorithm/string.hpp>
#include <fmt/format.h>
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<bool> {name});
SettingsVariable<bool>& 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<bool> {"alert_disabled"});
}
~Impl() {}
SettingsVariable<std::string> alertSoundFile_ {"alert_sound_file"};
SettingsVariable<std::string> alertLocationMethod_ {"alert_location_method"};
SettingsVariable<double> alertLatitude_ {"alert_latitude"};
SettingsVariable<double> alertLongitude_ {"alert_longitude"};
SettingsVariable<std::string> alertCounty_ {"alert_county"};
SettingsVariable<bool> ignoreMissingCodecs_ {"ignore_missing_codecs"};
std::unordered_map<awips::Phenomenon, SettingsVariable<bool>>
alertEnabled_ {};
std::vector<SettingsVariableBase*> variables_ {};
};
AudioSettings::AudioSettings() :
SettingsCategory("audio"), p(std::make_unique<Impl>())
{
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<std::string>& AudioSettings::alert_sound_file() const
{
return p->alertSoundFile_;
}
SettingsVariable<std::string>& AudioSettings::alert_location_method() const
{
return p->alertLocationMethod_;
}
SettingsVariable<double>& AudioSettings::alert_latitude() const
{
return p->alertLatitude_;
}
SettingsVariable<double>& AudioSettings::alert_longitude() const
{
return p->alertLongitude_;
}
SettingsVariable<std::string>& AudioSettings::alert_county() const
{
return p->alertCounty_;
}
SettingsVariable<bool>&
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<bool>& 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

View file

@ -0,0 +1,48 @@
#pragma once
#include <scwx/qt/settings/settings_category.hpp>
#include <scwx/qt/settings/settings_variable.hpp>
#include <scwx/awips/phenomenon.hpp>
#include <memory>
#include <string>
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<std::string>& alert_sound_file() const;
SettingsVariable<std::string>& alert_location_method() const;
SettingsVariable<double>& alert_latitude() const;
SettingsVariable<double>& alert_longitude() const;
SettingsVariable<std::string>& alert_county() const;
SettingsVariable<bool>& alert_enabled(awips::Phenomenon phenomenon) const;
SettingsVariable<bool>& ignore_missing_codecs() const;
static AudioSettings& Instance();
friend bool operator==(const AudioSettings& lhs, const AudioSettings& rhs);
private:
class Impl;
std::unique_ptr<Impl> p;
};
} // namespace settings
} // namespace qt
} // namespace scwx

View file

@ -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; \
}

View file

@ -180,6 +180,32 @@ void SettingsInterface<T>::SetEditWidget(QWidget* widget)
// TODO: Display invalid status
});
}
else if constexpr (std::is_same_v<T, double>)
{
// 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<T, std::vector<std::int64_t>>)
{
// If the line is edited (not programatically changed), stage the new
@ -310,6 +336,52 @@ void SettingsInterface<T>::SetEditWidget(QWidget* widget)
});
}
}
else if (QDoubleSpinBox* doubleSpinBox =
dynamic_cast<QDoubleSpinBox*>(widget))
{
if constexpr (std::is_floating_point_v<T>)
{
const std::optional<T> minimum = p->variable_->GetMinimum();
const std::optional<T> maximum = p->variable_->GetMaximum();
if (minimum.has_value())
{
doubleSpinBox->setMinimum(static_cast<double>(*minimum));
}
if (maximum.has_value())
{
doubleSpinBox->setMaximum(static_cast<double>(*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<T> 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<T>(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<T>(d) != *staged)
{
p->stagedValid_ = p->variable_->StageValue(static_cast<T>(d));
p->UpdateResetButton();
}
// Otherwise, don't process an unchanged value
});
}
}
p->UpdateEditWidget();
}
@ -378,6 +450,10 @@ void SettingsInterface<T>::Impl::SetWidgetText(U* widget, const T& currentValue)
{
widget->setText(QString::number(currentValue));
}
else if constexpr (std::is_floating_point_v<T>)
{
widget->setText(QString::number(currentValue));
}
else if constexpr (std::is_same_v<T, std::string>)
{
if (mapFromValue_ != nullptr)
@ -448,6 +524,14 @@ void SettingsInterface<T>::Impl::UpdateEditWidget()
spinBox->setValue(static_cast<int>(currentValue));
}
}
else if (QDoubleSpinBox* doubleSpinBox =
dynamic_cast<QDoubleSpinBox*>(editWidget_))
{
if constexpr (std::is_floating_point_v<T>)
{
doubleSpinBox->setValue(static_cast<double>(currentValue));
}
}
}
template<class T>

View file

@ -37,6 +37,17 @@ std::string GetAlertActionName(AlertAction alertAction)
return alertActionName_.at(alertAction);
}
const std::vector<awips::Phenomenon>& GetAlertAudioPhenomena()
{
static const std::vector<awips::Phenomenon> phenomena_ {
awips::Phenomenon::FlashFlood,
awips::Phenomenon::SevereThunderstorm,
awips::Phenomenon::SnowSquall,
awips::Phenomenon::Tornado};
return phenomena_;
}
} // namespace types
} // namespace qt
} // namespace scwx

View file

@ -1,8 +1,10 @@
#pragma once
#include <scwx/awips/phenomenon.hpp>
#include <scwx/util/iterator.hpp>
#include <string>
#include <vector>
namespace scwx
{
@ -23,6 +25,8 @@ typedef scwx::util::Iterator<AlertAction, AlertAction::Go, AlertAction::View>
AlertAction GetAlertAction(const std::string& name);
std::string GetAlertActionName(AlertAction alertAction);
const std::vector<awips::Phenomenon>& GetAlertAudioPhenomena();
} // namespace types
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,30 @@
#include <scwx/qt/types/location_types.hpp>
#include <scwx/util/enum.hpp>
#include <unordered_map>
#include <boost/algorithm/string.hpp>
namespace scwx
{
namespace qt
{
namespace types
{
static const std::unordered_map<LocationMethod, std::string>
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

View file

@ -0,0 +1,30 @@
#pragma once
#include <scwx/util/iterator.hpp>
#include <string>
namespace scwx
{
namespace qt
{
namespace types
{
enum class LocationMethod
{
Fixed,
Track,
County,
Unknown
};
typedef scwx::util::
Iterator<LocationMethod, LocationMethod::Fixed, LocationMethod::County>
LocationMethodIterator;
LocationMethod GetLocationMethod(const std::string& name);
const std::string& GetLocationMethodName(LocationMethod locationMethod);
} // namespace types
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,24 @@
#include <scwx/qt/types/media_types.hpp>
#include <unordered_map>
namespace scwx
{
namespace qt
{
namespace types
{
static const std::unordered_map<AudioFile, std::string> 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

View file

@ -0,0 +1,27 @@
#pragma once
#include <scwx/util/iterator.hpp>
#include <string>
namespace scwx
{
namespace qt
{
namespace types
{
enum class AudioFile
{
EasAttentionSignal
};
typedef scwx::util::Iterator<AudioFile,
AudioFile::EasAttentionSignal,
AudioFile::EasAttentionSignal>
AudioFileIterator;
const std::string& GetMediaPath(AudioFile audioFile);
} // namespace types
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,175 @@
#include "county_dialog.hpp"
#include "ui_county_dialog.h"
#include <scwx/qt/config/county_database.hpp>
#include <scwx/util/logger.hpp>
#include <QPushButton>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
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<std::string, std::string>& states_;
};
CountyDialog::CountyDialog(QWidget* parent) :
QDialog(parent), p {std::make_unique<Impl>(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<std::string, std::string>& 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

View file

@ -0,0 +1,37 @@
#pragma once
#include <QDialog>
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<Impl> p;
Ui::CountyDialog* ui;
};
} // namespace ui
} // namespace qt
} // namespace scwx

View file

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CountyDialog</class>
<widget class="QDialog" name="CountyDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>400</height>
</rect>
</property>
<property name="windowTitle">
<string>Select County</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeView" name="countyView">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="indentation">
<number>0</number>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QComboBox" name="stateComboBox"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CountyDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CountyDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -3,17 +3,23 @@
#include <scwx/awips/phenomenon.hpp>
#include <scwx/common/color_table.hpp>
#include <scwx/qt/config/county_database.hpp>
#include <scwx/qt/config/radar_site.hpp>
#include <scwx/qt/manager/media_manager.hpp>
#include <scwx/qt/manager/position_manager.hpp>
#include <scwx/qt/manager/settings_manager.hpp>
#include <scwx/qt/map/map_provider.hpp>
#include <scwx/qt/settings/audio_settings.hpp>
#include <scwx/qt/settings/general_settings.hpp>
#include <scwx/qt/settings/palette_settings.hpp>
#include <scwx/qt/settings/settings_interface.hpp>
#include <scwx/qt/settings/text_settings.hpp>
#include <scwx/qt/types/alert_types.hpp>
#include <scwx/qt/types/font_types.hpp>
#include <scwx/qt/types/location_types.hpp>
#include <scwx/qt/types/qt_types.hpp>
#include <scwx/qt/types/text_types.hpp>
#include <scwx/qt/ui/county_dialog.hpp>
#include <scwx/qt/ui/radar_site_dialog.hpp>
#include <scwx/qt/util/color.hpp>
#include <scwx/qt/util/file.hpp>
@ -26,6 +32,7 @@
#include <QFileDialog>
#include <QFontDatabase>
#include <QFontDialog>
#include <QGeoPositionInfo>
#include <QStandardItemModel>
#include <QToolButton>
@ -84,12 +91,31 @@ static const std::unordered_map<std::string, ColorTableConversions>
{"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<settings::SettingsInterfaceBase*> {
@ -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<manager::MediaManager> mediaManager_ {
manager::MediaManager::Instance()};
std::shared_ptr<manager::PositionManager> positionManager_ {
manager::PositionManager::Instance()};
settings::SettingsInterface<std::string> defaultRadarSite_ {};
settings::SettingsInterface<std::int64_t> gridWidth_ {};
settings::SettingsInterface<std::int64_t> gridHeight_ {};
@ -191,6 +229,15 @@ public:
settings::SettingsInterface<std::string>>
inactiveAlertColors_ {};
settings::SettingsInterface<std::string> alertAudioSoundFile_ {};
settings::SettingsInterface<std::string> alertAudioLocationMethod_ {};
settings::SettingsInterface<double> alertAudioLatitude_ {};
settings::SettingsInterface<double> alertAudioLongitude_ {};
settings::SettingsInterface<std::string> alertAudioCounty_ {};
std::unordered_map<awips::Phenomenon, settings::SettingsInterface<bool>>
alertAudioEnabled_ {};
std::unordered_map<types::FontCategory,
settings::SettingsInterface<std::string>>
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<QGridLayout*>(self_->ui->alertAudioGroupBox->layout());
for (const auto& phenomenon : types::GetAlertAudioPhenomena())
{
QCheckBox* alertAudioCheckbox = new QCheckBox(self_);
alertAudioCheckbox->setText(
QString::fromStdString(awips::GetPhenomenonText(phenomenon)));
static_cast<QGridLayout*>(self_->ui->alertAudioGroupBox->layout())
->addWidget(
alertAudioCheckbox, alertAudioLayout->rowCount(), 0, 1, -1);
// Create settings interface
auto result = alertAudioEnabled_.emplace(
phenomenon, settings::SettingsInterface<bool> {});
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();

View file

@ -77,6 +77,15 @@
<normaloff>:/res/icons/font-awesome-6/palette-solid.svg</normaloff>:/res/icons/font-awesome-6/palette-solid.svg</iconset>
</property>
</item>
<item>
<property name="text">
<string>Audio</string>
</property>
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/volume-high-solid.svg</normaloff>:/res/icons/font-awesome-6/volume-high-solid.svg</iconset>
</property>
</item>
<item>
<property name="text">
<string>Text</string>
@ -364,8 +373,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>63</width>
<height>18</height>
<width>508</width>
<height>383</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_3">
@ -436,6 +445,223 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="page">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QGroupBox" name="alertAudioGroupBox">
<property name="title">
<string>Alerts</string>
</property>
<layout class="QGridLayout" name="gridLayout_10">
<item row="2" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Latitude</string>
</property>
</widget>
</item>
<item row="1" column="6">
<widget class="QToolButton" name="resetAlertAudioLocationMethodButton">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/rotate-left-solid.svg</normaloff>:/res/icons/font-awesome-6/rotate-left-solid.svg</iconset>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Longitude</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QToolButton" name="alertAudioSoundSelectButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>Location Method</string>
</property>
</widget>
</item>
<item row="3" column="6">
<widget class="QToolButton" name="resetAlertAudioLongitudeButton">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/rotate-left-solid.svg</normaloff>:/res/icons/font-awesome-6/rotate-left-solid.svg</iconset>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QToolButton" name="alertAudioSoundStopButton">
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/stop-solid.svg</normaloff>:/res/icons/font-awesome-6/stop-solid.svg</iconset>
</property>
</widget>
</item>
<item row="2" column="6">
<widget class="QToolButton" name="resetAlertAudioLatitudeButton">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/rotate-left-solid.svg</normaloff>:/res/icons/font-awesome-6/rotate-left-solid.svg</iconset>
</property>
</widget>
</item>
<item row="0" column="6">
<widget class="QToolButton" name="resetAlertAudioSoundButton">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/rotate-left-solid.svg</normaloff>:/res/icons/font-awesome-6/rotate-left-solid.svg</iconset>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QToolButton" name="alertAudioSoundTestButton">
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/play-solid.svg</normaloff>:/res/icons/font-awesome-6/play-solid.svg</iconset>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>Sound</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_19">
<property name="text">
<string>County</string>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QDoubleSpinBox" name="alertAudioLongitudeSpinBox">
<property name="decimals">
<number>4</number>
</property>
<property name="minimum">
<double>-180.000000000000000</double>
</property>
<property name="maximum">
<double>180.000000000000000</double>
</property>
<property name="singleStep">
<double>0.000100000000000</double>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="QDoubleSpinBox" name="alertAudioLatitudeSpinBox">
<property name="decimals">
<number>4</number>
</property>
<property name="minimum">
<double>-90.000000000000000</double>
</property>
<property name="maximum">
<double>90.000000000000000</double>
</property>
<property name="singleStep">
<double>0.000100000000000</double>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QComboBox" name="alertAudioLocationMethodComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QLineEdit" name="alertAudioSoundLineEdit"/>
</item>
<item row="4" column="6">
<widget class="QToolButton" name="resetAlertAudioCountyButton">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../scwx-qt.qrc">
<normaloff>:/res/icons/font-awesome-6/rotate-left-solid.svg</normaloff>:/res/icons/font-awesome-6/rotate-left-solid.svg</iconset>
</property>
</widget>
</item>
<item row="4" column="3">
<widget class="QToolButton" name="alertAudioCountySelectButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QLabel" name="alertAudioCountyLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="alertAudioCountyLineEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_6">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>253</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="text">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>

View file

@ -0,0 +1,176 @@
#include <scwx/qt/ui/setup/audio_codec_page.hpp>
#include <scwx/qt/manager/settings_manager.hpp>
#include <scwx/qt/settings/audio_settings.hpp>
#include <scwx/qt/settings/settings_interface.hpp>
#include <QCheckBox>
#include <QDesktopServices>
#include <QLabel>
#include <QMediaFormat>
#include <QScrollArea>
#include <QSpacerItem>
#include <QVBoxLayout>
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<bool> ignoreMissingCodecs_ {};
};
AudioCodecPage::AudioCodecPage(QWidget* parent) :
QWizardPage(parent), p {std::make_shared<Impl>(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(
"<p><b>Option 1</b></p>" //
"<p>Update your Windows installation. The required media codecs may "
"be available with the latest operating system updates.</p>" //
"<p><b>Option 2</b></p>" //
"<p>Install the <a "
"href=\"https://www.microsoft.com/store/productId/9N5TDP8VCMHS\">Web "
"Media Extensions</a> package from the Windows Store.</p>" //
"<p><b>Option 3</b></p>" //
"<p>Install <a "
"href=\"https://www.codecguide.com/"
"download_k-lite_codec_pack_basic.htm\">K-Lite Codec Pack "
"Basic</a>. This is a 3rd party application, and no support or warranty "
"is provided.</p>"));
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

View file

@ -0,0 +1,34 @@
#pragma once
#include <QWizardPage>
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<Impl> p;
};
} // namespace setup
} // namespace ui
} // namespace qt
} // namespace scwx

View file

@ -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

View file

@ -22,6 +22,8 @@ public:
bool isComplete() const override;
bool validatePage() override;
static bool IsRequired();
private:
class Impl;
std::shared_ptr<Impl> p;

View file

@ -1,9 +1,9 @@
#include <scwx/qt/ui/setup/setup_wizard.hpp>
#include <scwx/qt/ui/setup/audio_codec_page.hpp>
#include <scwx/qt/ui/setup/finish_page.hpp>
#include <scwx/qt/ui/setup/map_layout_page.hpp>
#include <scwx/qt/ui/setup/map_provider_page.hpp>
#include <scwx/qt/ui/setup/welcome_page.hpp>
#include <scwx/qt/settings/general_settings.hpp>
#include <QDesktopServices>
#include <QUrl>
@ -38,6 +38,7 @@ SetupWizard::SetupWizard(QWidget* parent) :
setPage(static_cast<int>(Page::Welcome), new WelcomePage(this));
setPage(static_cast<int>(Page::MapProvider), new MapProviderPage(this));
setPage(static_cast<int>(Page::MapLayout), new MapLayoutPage(this));
setPage(static_cast<int>(Page::AudioCodec), new AudioCodecPage(this));
setPage(static_cast<int>(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<int>(Page::MapProvider):
case static_cast<int>(Page::MapLayout):
if (MapProviderPage::IsRequired())
{
return nextId;
}
break;
case static_cast<int>(Page::AudioCodec):
if (AudioCodecPage::IsRequired())
{
return nextId;
}
break;
case static_cast<int>(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

View file

@ -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:

View file

@ -1,4 +1,9 @@
#include <scwx/qt/util/geographic_lib.hpp>
#include <scwx/util/logger.hpp>
#include <GeographicLib/Gnomonic.hpp>
#include <geos/algorithm/PointLocation.h>
#include <geos/geom/CoordinateSequence.h>
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<common::Coordinate>& 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<double>
GetAngle(double lat1, double lon1, double lat2, double lon2)
{

View file

@ -1,5 +1,9 @@
#pragma once
#include <scwx/common/geographic.hpp>
#include <vector>
#include <GeographicLib/Geodesic.hpp>
#include <units/angle.h>
#include <units/length.h>
@ -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<common::Coordinate>& area,
const common::Coordinate& point);
/**
* Get the angle between two points.
*

View file

@ -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)

@ -1 +1 @@
Subproject commit cd36a74a9c678d90d10ec397eae65b389a9640fc
Subproject commit 65bdc55e4afa29c24010a398238f2036060bbd0c

View file

@ -15,6 +15,12 @@ class CountyDatabaseTest :
virtual void SetUp() { scwx::qt::config::CountyDatabase::Initialize(); }
};
class CountyCountTest :
public testing::TestWithParam<std::pair<std::string, std::size_t>>
{
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

View file

@ -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<Type, std::string>& pair) -> bool \
{ return boost::iequals(pair.second, name); }); \
\
if (result != nameMap.cend()) \
{ \
return result->first; \
} \
else \
{ \
return Type::Unknown; \
} \
}

View file

@ -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