diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index 4cda1e58..718f0e3a 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -1466,6 +1466,7 @@ void MainWindowImpl::UpdateRadarSite() timelineManager_->SetRadarSite("?"); } + alertManager_->SetRadarSite(radarSite); placefileManager_->SetRadarSite(radarSite); } diff --git a/scwx-qt/source/scwx/qt/manager/alert_manager.cpp b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp index d68f9226..64c78d8d 100644 --- a/scwx-qt/source/scwx/qt/manager/alert_manager.cpp +++ b/scwx-qt/source/scwx/qt/manager/alert_manager.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include @@ -74,6 +76,8 @@ public: PositionManager::Instance()}; std::shared_ptr textEventManager_ { TextEventManager::Instance()}; + + std::shared_ptr radarSite_ {}; }; AlertManager::AlertManager() : p(std::make_unique(this)) {} @@ -100,6 +104,33 @@ common::Coordinate AlertManager::Impl::CurrentCoordinate( coordinate.longitude_ = trackedCoordinate.longitude(); } } + else if (locationMethod == types::LocationMethod::RadarSite) + { + std::string radarSiteSelection = + audioSettings.alert_radar_site().GetValue(); + std::shared_ptr radarSite; + if (radarSiteSelection == "default") + { + std::string siteId = settings::GeneralSettings::Instance() + .default_radar_site() + .GetValue(); + radarSite = config::RadarSite::Get(siteId); + } + else if (radarSiteSelection == "follow") + { + radarSite = radarSite_; + } + else + { + radarSite = config::RadarSite::Get(radarSiteSelection); + } + + if (radarSite != nullptr) + { + coordinate.latitude_ = radarSite->latitude(); + coordinate.longitude_ = radarSite->longitude(); + } + } return coordinate; } @@ -118,6 +149,8 @@ void AlertManager::Impl::HandleAlert(const types::TextEventKey& key, audioSettings.alert_location_method().GetValue()); common::Coordinate currentCoordinate = CurrentCoordinate(locationMethod); std::string alertCounty = audioSettings.alert_county().GetValue(); + auto alertRadius = units::length::kilometers( + audioSettings.alert_radius().GetValue()); auto message = textEventManager_->message_list(key).at(messageIndex); @@ -145,13 +178,16 @@ void AlertManager::Impl::HandleAlert(const types::TextEventKey& key, bool activeAtLocation = (locationMethod == types::LocationMethod::All); if (locationMethod == types::LocationMethod::Fixed || - locationMethod == types::LocationMethod::Track) + locationMethod == types::LocationMethod::Track || + locationMethod == types::LocationMethod::RadarSite) { // Determine if the alert is active at the current coordinte auto alertCoordinates = segment->codedLocation_->coordinates(); - activeAtLocation = util::GeographicLib::AreaContainsPoint( - alertCoordinates, currentCoordinate); + activeAtLocation = util::GeographicLib::AreaInRangeOfPoint( + alertCoordinates, + currentCoordinate, + alertRadius); } else if (locationMethod == types::LocationMethod::County) { @@ -183,6 +219,27 @@ void AlertManager::Impl::UpdateLocationTracking( positionManager_->EnablePositionUpdates(uuid_, locationEnabled); } +void AlertManager::SetRadarSite( + const std::shared_ptr& radarSite) +{ + if (p->radarSite_ == radarSite) + { + // No action needed + return; + } + + if (radarSite == nullptr) + { + logger_->debug("SetRadarSite: ?"); + } + else + { + logger_->debug("SetRadarSite: {}", radarSite->id()); + } + + p->radarSite_ = radarSite; +} + std::shared_ptr AlertManager::Instance() { static std::weak_ptr alertManagerReference_ {}; diff --git a/scwx-qt/source/scwx/qt/manager/alert_manager.hpp b/scwx-qt/source/scwx/qt/manager/alert_manager.hpp index 5bdfd923..f81054c7 100644 --- a/scwx-qt/source/scwx/qt/manager/alert_manager.hpp +++ b/scwx-qt/source/scwx/qt/manager/alert_manager.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -20,6 +22,7 @@ public: explicit AlertManager(); ~AlertManager(); + void SetRadarSite(const std::shared_ptr& radarSite); static std::shared_ptr Instance(); private: diff --git a/scwx-qt/source/scwx/qt/model/alert_model.cpp b/scwx-qt/source/scwx/qt/model/alert_model.cpp index 76cd5a5c..d4bb1111 100644 --- a/scwx-qt/source/scwx/qt/model/alert_model.cpp +++ b/scwx-qt/source/scwx/qt/model/alert_model.cpp @@ -1,13 +1,16 @@ #include #include #include +#include #include +#include #include #include #include #include #include + #include #include @@ -73,7 +76,6 @@ public: double, types::TextEventHash> distanceMap_; - scwx::common::DistanceType distanceDisplay_; scwx::common::Coordinate previousPosition_; }; @@ -182,18 +184,19 @@ QVariant AlertModel::data(const QModelIndex& index, int role) const case static_cast(Column::Distance): if (role == Qt::DisplayRole) { - if (p->distanceDisplay_ == scwx::common::DistanceType::Miles) - { - return QString("%1 mi").arg( - static_cast(p->distanceMap_.at(textEventKey) * - scwx::common::kMilesPerMeter)); - } - else - { - return QString("%1 km").arg( - static_cast(p->distanceMap_.at(textEventKey) * - scwx::common::kKilometersPerMeter)); - } + const std::string distanceUnitName = + settings::UnitSettings::Instance().distance_units().GetValue(); + types::DistanceUnits distanceUnits = + types::GetDistanceUnitsFromName(distanceUnitName); + double distanceScale = types::GetDistanceUnitsScale(distanceUnits); + std::string abbreviation = + types::GetDistanceUnitsAbbreviation(distanceUnits); + + return QString("%1 %2") + .arg(static_cast(p->distanceMap_.at(textEventKey) * + scwx::common::kKilometersPerMeter * + distanceScale)) + .arg(QString::fromStdString(abbreviation)); } else { @@ -419,7 +422,6 @@ AlertModelImpl::AlertModelImpl() : textEventKeys_ {}, geodesic_(util::GeographicLib::DefaultGeodesic()), distanceMap_ {}, - distanceDisplay_ {scwx::common::DistanceType::Miles}, previousPosition_ {} { } diff --git a/scwx-qt/source/scwx/qt/model/radar_site_model.cpp b/scwx-qt/source/scwx/qt/model/radar_site_model.cpp index 66683ae3..482c9828 100644 --- a/scwx-qt/source/scwx/qt/model/radar_site_model.cpp +++ b/scwx-qt/source/scwx/qt/model/radar_site_model.cpp @@ -1,6 +1,8 @@ #include #include +#include #include +#include #include #include #include @@ -36,7 +38,6 @@ public: radarSites_ {}, geodesic_(util::GeographicLib::DefaultGeodesic()), distanceMap_ {}, - distanceDisplay_ {scwx::common::DistanceType::Miles}, previousPosition_ {} { // Get all loaded radar sites @@ -64,7 +65,6 @@ public: const GeographicLib::Geodesic& geodesic_; std::unordered_map distanceMap_; - scwx::common::DistanceType distanceDisplay_; scwx::common::Coordinate previousPosition_; QIcon starIcon_ {":/res/icons/font-awesome-6/star-solid.svg"}; @@ -213,18 +213,19 @@ QVariant RadarSiteModel::data(const QModelIndex& index, int role) const case static_cast(Column::Distance): if (role == Qt::DisplayRole) { - if (p->distanceDisplay_ == scwx::common::DistanceType::Miles) - { - return QString("%1 mi").arg( - static_cast(p->distanceMap_.at(site->id()) * - scwx::common::kMilesPerMeter)); - } - else - { - return QString("%1 km").arg( - static_cast(p->distanceMap_.at(site->id()) * - scwx::common::kKilometersPerMeter)); - } + const std::string distanceUnitName = + settings::UnitSettings::Instance().distance_units().GetValue(); + types::DistanceUnits distanceUnits = + types::GetDistanceUnitsFromName(distanceUnitName); + double distanceScale = types::GetDistanceUnitsScale(distanceUnits); + std::string abbreviation = + types::GetDistanceUnitsAbbreviation(distanceUnits); + + return QString("%1 %2") + .arg(static_cast(p->distanceMap_.at(site->id()) * + scwx::common::kKilometersPerMeter * + distanceScale)) + .arg(QString::fromStdString(abbreviation)); } else { diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp index 0ee04b2b..995ce881 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.cpp @@ -37,12 +37,17 @@ public: alertLocationMethod_.SetDefault(defaultAlertLocationMethodValue); alertLatitude_.SetDefault(0.0); alertLongitude_.SetDefault(0.0); + alertRadius_.SetDefault(0.0); + alertRadarSite_.SetDefault("default"); ignoreMissingCodecs_.SetDefault(false); alertLatitude_.SetMinimum(-90.0); alertLatitude_.SetMaximum(90.0); alertLongitude_.SetMinimum(-180.0); alertLongitude_.SetMaximum(180.0); + alertRadius_.SetMinimum(0.0); + alertRadius_.SetMaximum(9999999999); + alertLocationMethod_.SetValidator( SCWX_SETTINGS_ENUM_VALIDATOR(types::LocationMethod, @@ -86,6 +91,8 @@ public: SettingsVariable alertLocationMethod_ {"alert_location_method"}; SettingsVariable alertLatitude_ {"alert_latitude"}; SettingsVariable alertLongitude_ {"alert_longitude"}; + SettingsVariable alertRadarSite_ {"alert_radar_site"}; + SettingsVariable alertRadius_ {"alert_radius"}; SettingsVariable alertCounty_ {"alert_county"}; SettingsVariable ignoreMissingCodecs_ {"ignore_missing_codecs"}; @@ -101,6 +108,8 @@ AudioSettings::AudioSettings() : &p->alertLocationMethod_, &p->alertLatitude_, &p->alertLongitude_, + &p->alertRadarSite_, + &p->alertRadius_, &p->alertCounty_, &p->ignoreMissingCodecs_}); RegisterVariables(p->variables_); @@ -133,6 +142,16 @@ SettingsVariable& AudioSettings::alert_longitude() const return p->alertLongitude_; } +SettingsVariable& AudioSettings::alert_radar_site() const +{ + return p->alertRadarSite_; +} + +SettingsVariable& AudioSettings::alert_radius() const +{ + return p->alertRadius_; +} + SettingsVariable& AudioSettings::alert_county() const { return p->alertCounty_; @@ -166,6 +185,8 @@ bool operator==(const AudioSettings& lhs, const AudioSettings& rhs) lhs.p->alertLocationMethod_ == rhs.p->alertLocationMethod_ && lhs.p->alertLatitude_ == rhs.p->alertLatitude_ && lhs.p->alertLongitude_ == rhs.p->alertLongitude_ && + lhs.p->alertRadarSite_ == rhs.p->alertRadarSite_ && + lhs.p->alertRadius_ == rhs.p->alertRadius_ && lhs.p->alertCounty_ == rhs.p->alertCounty_ && lhs.p->alertEnabled_ == rhs.p->alertEnabled_); } diff --git a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp index 19012e84..466c4f3a 100644 --- a/scwx-qt/source/scwx/qt/settings/audio_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/audio_settings.hpp @@ -30,6 +30,8 @@ public: SettingsVariable& alert_location_method() const; SettingsVariable& alert_latitude() const; SettingsVariable& alert_longitude() const; + SettingsVariable& alert_radius() const; + SettingsVariable& alert_radar_site() const; SettingsVariable& alert_county() const; SettingsVariable& alert_enabled(awips::Phenomenon phenomenon) const; SettingsVariable& ignore_missing_codecs() const; diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp index 2bf38e80..9a24c5ae 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.cpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.cpp @@ -40,6 +40,7 @@ public: void UpdateEditWidget(); void UpdateResetButton(); + void UpdateUnitLabel(); SettingsInterface* self_; @@ -49,9 +50,14 @@ public: std::unique_ptr context_ {std::make_unique()}; QWidget* editWidget_ {nullptr}; QAbstractButton* resetButton_ {nullptr}; + QLabel* unitLabel_ {nullptr}; std::function mapFromValue_ {nullptr}; std::function mapToValue_ {nullptr}; + + double unitScale_ {1}; + std::optional unitAbbreviation_ {}; + bool unitEnabled_ {false}; }; template @@ -381,6 +387,11 @@ void SettingsInterface::SetEditWidget(QWidget* widget) p->context_.get(), [this](double d) { + if (p->unitEnabled_) + { + d = d / p->unitScale_; + } + const T value = p->variable_->GetValue(); const std::optional staged = p->variable_->GetStaged(); @@ -448,6 +459,11 @@ void SettingsInterface::SetResetButton(QAbstractButton* button) p->UpdateResetButton(); } } +template +void SettingsInterface::SetUnitLabel(QLabel* label) +{ + p->unitLabel_ = label; +} template void SettingsInterface::SetMapFromValueFunction( @@ -463,6 +479,17 @@ void SettingsInterface::SetMapToValueFunction( p->mapToValue_ = function; } +template +void SettingsInterface::SetUnit(const double& scale, + const std::string& abbreviation) +{ + p->unitScale_ = scale; + p->unitAbbreviation_ = abbreviation; + p->unitEnabled_ = true; + p->UpdateEditWidget(); + p->UpdateUnitLabel(); +} + template template void SettingsInterface::Impl::SetWidgetText(U* widget, const T& currentValue) @@ -559,11 +586,27 @@ void SettingsInterface::Impl::UpdateEditWidget() { if constexpr (std::is_floating_point_v) { - doubleSpinBox->setValue(static_cast(currentValue)); + double doubleValue = static_cast(currentValue); + if (unitEnabled_) + { + doubleValue = doubleValue * unitScale_; + } + doubleSpinBox->setValue(doubleValue); } } } +template +void SettingsInterface::Impl::UpdateUnitLabel() +{ + if (unitLabel_ == nullptr || !unitEnabled_) + { + return; + } + + unitLabel_->setText(QString::fromStdString(unitAbbreviation_.value_or(""))); +} + template void SettingsInterface::Impl::UpdateResetButton() { diff --git a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp index f5f5bb4a..b049dcc1 100644 --- a/scwx-qt/source/scwx/qt/settings/settings_interface.hpp +++ b/scwx-qt/source/scwx/qt/settings/settings_interface.hpp @@ -7,6 +7,8 @@ #include #include +class QLabel; + namespace scwx { namespace qt @@ -91,6 +93,13 @@ public: */ void SetResetButton(QAbstractButton* button) override; + /** + * Sets the label for units from the settings dialog. + * + * @param label Unit label + */ + void SetUnitLabel(QLabel* label); + /** * If the edit widget displays a different value than what is stored in the * settings variable, a mapping function must be provided in order to convert @@ -109,6 +118,14 @@ public: */ void SetMapToValueFunction(std::function function); + /** + * Sets the unit to be used by this setting. + * + * @param scale The radio of the current unit to the base unit + * @param abbreviation The abreviation to be displayed + */ + void SetUnit(const double& scale, const std::string& abbreviation); + private: class Impl; std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/settings/unit_settings.cpp b/scwx-qt/source/scwx/qt/settings/unit_settings.cpp index e4cf4073..c2cb0f11 100644 --- a/scwx-qt/source/scwx/qt/settings/unit_settings.cpp +++ b/scwx-qt/source/scwx/qt/settings/unit_settings.cpp @@ -26,16 +26,20 @@ public: types::GetOtherUnitsName(types::OtherUnits::Default); std::string defaultSpeedUnitsValue = types::GetSpeedUnitsName(types::SpeedUnits::Knots); + std::string defaultDistanceUnitsValue = + types::GetDistanceUnitsName(types::DistanceUnits::Miles); boost::to_lower(defaultAccumulationUnitsValue); boost::to_lower(defaultEchoTopsUnitsValue); boost::to_lower(defaultOtherUnitsValue); boost::to_lower(defaultSpeedUnitsValue); + boost::to_lower(defaultDistanceUnitsValue); accumulationUnits_.SetDefault(defaultAccumulationUnitsValue); echoTopsUnits_.SetDefault(defaultEchoTopsUnitsValue); otherUnits_.SetDefault(defaultOtherUnitsValue); speedUnits_.SetDefault(defaultSpeedUnitsValue); + distanceUnits_.SetDefault(defaultDistanceUnitsValue); accumulationUnits_.SetValidator( SCWX_SETTINGS_ENUM_VALIDATOR(types::AccumulationUnits, @@ -53,6 +57,10 @@ public: SCWX_SETTINGS_ENUM_VALIDATOR(types::SpeedUnits, types::SpeedUnitsIterator(), types::GetSpeedUnitsName)); + distanceUnits_.SetValidator( + SCWX_SETTINGS_ENUM_VALIDATOR(types::DistanceUnits, + types::DistanceUnitsIterator(), + types::GetDistanceUnitsName)); } ~Impl() {} @@ -61,6 +69,7 @@ public: SettingsVariable echoTopsUnits_ {"echo_tops_units"}; SettingsVariable otherUnits_ {"other_units"}; SettingsVariable speedUnits_ {"speed_units"}; + SettingsVariable distanceUnits_ {"distance_units"}; }; UnitSettings::UnitSettings() : @@ -69,7 +78,8 @@ UnitSettings::UnitSettings() : RegisterVariables({&p->accumulationUnits_, &p->echoTopsUnits_, &p->otherUnits_, - &p->speedUnits_}); + &p->speedUnits_, + &p->distanceUnits_}); SetDefaults(); } UnitSettings::~UnitSettings() = default; @@ -97,6 +107,11 @@ SettingsVariable& UnitSettings::speed_units() const return p->speedUnits_; } +SettingsVariable& UnitSettings::distance_units() const +{ + return p->distanceUnits_; +} + UnitSettings& UnitSettings::Instance() { static UnitSettings generalSettings_; @@ -108,7 +123,8 @@ bool operator==(const UnitSettings& lhs, const UnitSettings& rhs) return (lhs.p->accumulationUnits_ == rhs.p->accumulationUnits_ && lhs.p->echoTopsUnits_ == rhs.p->echoTopsUnits_ && lhs.p->otherUnits_ == rhs.p->otherUnits_ && - lhs.p->speedUnits_ == rhs.p->speedUnits_); + lhs.p->speedUnits_ == rhs.p->speedUnits_ && + lhs.p->distanceUnits_ == rhs.p->distanceUnits_); } } // namespace settings diff --git a/scwx-qt/source/scwx/qt/settings/unit_settings.hpp b/scwx-qt/source/scwx/qt/settings/unit_settings.hpp index c849cffb..15518492 100644 --- a/scwx-qt/source/scwx/qt/settings/unit_settings.hpp +++ b/scwx-qt/source/scwx/qt/settings/unit_settings.hpp @@ -29,6 +29,7 @@ public: SettingsVariable& echo_tops_units() const; SettingsVariable& other_units() const; SettingsVariable& speed_units() const; + SettingsVariable& distance_units() const; static UnitSettings& Instance(); diff --git a/scwx-qt/source/scwx/qt/types/location_types.cpp b/scwx-qt/source/scwx/qt/types/location_types.cpp index b1c759de..f9df0189 100644 --- a/scwx-qt/source/scwx/qt/types/location_types.cpp +++ b/scwx-qt/source/scwx/qt/types/location_types.cpp @@ -15,6 +15,7 @@ namespace types static const std::unordered_map locationMethodName_ {{LocationMethod::Fixed, "Fixed"}, {LocationMethod::Track, "Track"}, + {LocationMethod::RadarSite, "Radar Site"}, {LocationMethod::County, "County"}, {LocationMethod::All, "All"}, {LocationMethod::Unknown, "?"}}; diff --git a/scwx-qt/source/scwx/qt/types/location_types.hpp b/scwx-qt/source/scwx/qt/types/location_types.hpp index 65d32f1e..4da1e701 100644 --- a/scwx-qt/source/scwx/qt/types/location_types.hpp +++ b/scwx-qt/source/scwx/qt/types/location_types.hpp @@ -15,6 +15,7 @@ enum class LocationMethod { Fixed, Track, + RadarSite, County, All, Unknown diff --git a/scwx-qt/source/scwx/qt/types/unit_types.cpp b/scwx-qt/source/scwx/qt/types/unit_types.cpp index fbab05da..09f532b5 100644 --- a/scwx-qt/source/scwx/qt/types/unit_types.cpp +++ b/scwx-qt/source/scwx/qt/types/unit_types.cpp @@ -89,12 +89,35 @@ static const std::unordered_map speedUnitsScale_ { {SpeedUnits::User, 1.0f}, {SpeedUnits::Unknown, 1.0f}}; +static const std::unordered_map + distanceUnitsAbbreviation_ {{DistanceUnits::Kilometers, "km"}, + {DistanceUnits::Miles, "mi"}, + {DistanceUnits::User, ""}, + {DistanceUnits::Unknown, ""}}; + +static const std::unordered_map distanceUnitsName_ { + {DistanceUnits::Kilometers, "Kilometers"}, + {DistanceUnits::Miles, "Miles"}, + {DistanceUnits::User, "User-defined"}, + {DistanceUnits::Unknown, "?"}}; + +static constexpr auto distanceUnitsBase_ = units::kilometers {1.0f}; +static const std::unordered_map distanceUnitsScale_ { + {DistanceUnits::Kilometers, + (distanceUnitsBase_ / units::kilometers {1.0f})}, + {DistanceUnits::Miles, + (distanceUnitsBase_ / units::miles {1.0f})}, + {DistanceUnits::User, 1.0f}, + {DistanceUnits::Unknown, 1.0f}}; + + SCWX_GET_ENUM(AccumulationUnits, GetAccumulationUnitsFromName, accumulationUnitsName_) SCWX_GET_ENUM(EchoTopsUnits, GetEchoTopsUnitsFromName, echoTopsUnitsName_) SCWX_GET_ENUM(OtherUnits, GetOtherUnitsFromName, otherUnitsName_) SCWX_GET_ENUM(SpeedUnits, GetSpeedUnitsFromName, speedUnitsName_) +SCWX_GET_ENUM(DistanceUnits, GetDistanceUnitsFromName, distanceUnitsName_) const std::string& GetAccumulationUnitsAbbreviation(AccumulationUnits units) { @@ -146,6 +169,21 @@ float GetSpeedUnitsScale(SpeedUnits units) return speedUnitsScale_.at(units); } +const std::string& GetDistanceUnitsAbbreviation(DistanceUnits units) +{ + return distanceUnitsAbbreviation_.at(units); +} + +const std::string& GetDistanceUnitsName(DistanceUnits units) +{ + return distanceUnitsName_.at(units); +} + +double GetDistanceUnitsScale(DistanceUnits units) +{ + return distanceUnitsScale_.at(units); +} + } // namespace types } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/types/unit_types.hpp b/scwx-qt/source/scwx/qt/types/unit_types.hpp index fd1a012b..ca29623f 100644 --- a/scwx-qt/source/scwx/qt/types/unit_types.hpp +++ b/scwx-qt/source/scwx/qt/types/unit_types.hpp @@ -56,6 +56,17 @@ typedef scwx::util:: Iterator SpeedUnitsIterator; +enum class DistanceUnits +{ + Kilometers, + Miles, + User, + Unknown +}; +typedef scwx::util:: + Iterator + DistanceUnitsIterator; + const std::string& GetAccumulationUnitsAbbreviation(AccumulationUnits units); const std::string& GetAccumulationUnitsName(AccumulationUnits units); AccumulationUnits GetAccumulationUnitsFromName(const std::string& name); @@ -74,6 +85,11 @@ const std::string& GetSpeedUnitsName(SpeedUnits units); SpeedUnits GetSpeedUnitsFromName(const std::string& name); float GetSpeedUnitsScale(SpeedUnits units); +const std::string& GetDistanceUnitsAbbreviation(DistanceUnits units); +const std::string& GetDistanceUnitsName(DistanceUnits units); +DistanceUnits GetDistanceUnitsFromName(const std::string& name); +double GetDistanceUnitsScale(DistanceUnits units); + } // namespace types } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/ui/settings/unit_settings_widget.cpp b/scwx-qt/source/scwx/qt/ui/settings/unit_settings_widget.cpp index 946d21ce..aff11583 100644 --- a/scwx-qt/source/scwx/qt/ui/settings/unit_settings_widget.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings/unit_settings_widget.cpp @@ -102,6 +102,16 @@ public: types::GetSpeedUnitsName); AddRow(speedUnits_, "Speed", speedComboBox); + QComboBox* distanceComboBox = new QComboBox(self); + distanceComboBox->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Preferred); + distanceUnits_.SetSettingsVariable(unitSettings.distance_units()); + SCWX_SETTINGS_COMBO_BOX(distanceUnits_, + distanceComboBox, + types::DistanceUnitsIterator(), + types::GetDistanceUnitsName); + AddRow(distanceUnits_, "Distance", distanceComboBox); + QComboBox* otherComboBox = new QComboBox(self); otherComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); @@ -127,6 +137,7 @@ public: settings::SettingsInterface echoTopsUnits_ {}; settings::SettingsInterface otherUnits_ {}; settings::SettingsInterface speedUnits_ {}; + settings::SettingsInterface distanceUnits_ {}; }; UnitSettingsWidget::UnitSettingsWidget(QWidget* parent) : diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp index bcebaf0a..d36efb07 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.cpp @@ -14,12 +14,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -103,6 +105,7 @@ public: explicit SettingsDialogImpl(SettingsDialog* self) : self_ {self}, radarSiteDialog_ {new RadarSiteDialog(self)}, + alertAudioRadarSiteDialog_ {new RadarSiteDialog(self)}, gpsSourceDialog_ {new SerialPortDialog(self)}, countyDialog_ {new CountyDialog(self)}, fontDialog_ {new QFontDialog(self)}, @@ -134,6 +137,8 @@ public: &alertAudioLocationMethod_, &alertAudioLatitude_, &alertAudioLongitude_, + &alertAudioRadarSite_, + &alertAudioRadius_, &alertAudioCounty_, &hoverTextWrap_, &tooltipMethod_, @@ -175,6 +180,7 @@ public: void ShowColorDialog(QLineEdit* lineEdit, QFrame* frame = nullptr); void UpdateRadarDialogLocation(const std::string& id); + void UpdateAlertRadarDialogLocation(const std::string& id); QFont GetSelectedFont(); void SelectFontCategory(types::FontCategory fontCategory); @@ -199,6 +205,7 @@ public: SettingsDialog* self_; RadarSiteDialog* radarSiteDialog_; + RadarSiteDialog* alertAudioRadarSiteDialog_; SerialPortDialog* gpsSourceDialog_; CountyDialog* countyDialog_; QFontDialog* fontDialog_; @@ -252,6 +259,8 @@ public: settings::SettingsInterface alertAudioLocationMethod_ {}; settings::SettingsInterface alertAudioLatitude_ {}; settings::SettingsInterface alertAudioLongitude_ {}; + settings::SettingsInterface alertAudioRadarSite_ {}; + settings::SettingsInterface alertAudioRadius_ {}; settings::SettingsInterface alertAudioCounty_ {}; std::unordered_map> @@ -339,6 +348,30 @@ void SettingsDialogImpl::ConnectSignals() } }); + QObject::connect(self_->ui->alertAudioRadarSiteSelectButton, + &QAbstractButton::clicked, + self_, + [this]() { alertAudioRadarSiteDialog_->show(); }); + + QObject::connect(alertAudioRadarSiteDialog_, + &RadarSiteDialog::accepted, + self_, + [this]() + { + std::string id = + alertAudioRadarSiteDialog_->radar_site(); + + std::shared_ptr radarSite = + config::RadarSite::Get(id); + + if (radarSite != nullptr) + { + self_->ui->alertAudioRadarSiteComboBox + ->setCurrentText(QString::fromStdString( + RadarSiteLabel(radarSite))); + } + }); + QObject::connect(self_->ui->gpsSourceSelectButton, &QAbstractButton::clicked, self_, @@ -504,11 +537,18 @@ void SettingsDialogImpl::SetupGeneralTab() const std::shared_ptr& b) { return a->id() < b->id(); }); + // Add default and follow options to radar sites + self_->ui->alertAudioRadarSiteComboBox->addItem( + QString::fromStdString("default")); + self_->ui->alertAudioRadarSiteComboBox->addItem( + QString::fromStdString("follow")); + // Add sorted radar sites for (std::shared_ptr& radarSite : radarSites) { QString text = QString::fromStdString(RadarSiteLabel(radarSite)); self_->ui->radarSiteComboBox->addItem(text); + self_->ui->alertAudioRadarSiteComboBox->addItem(text); } defaultRadarSite_.SetSettingsVariable(generalSettings.default_radar_site()); @@ -900,6 +940,12 @@ void SettingsDialogImpl::SetupAudioTab() bool coordinateEntryEnabled = locationMethod == types::LocationMethod::Fixed; + bool radarSiteEntryEnable = + locationMethod == types::LocationMethod::RadarSite; + bool radiusEntryEnable = + locationMethod == types::LocationMethod::Fixed || + locationMethod == types::LocationMethod::Track || + locationMethod == types::LocationMethod::RadarSite; bool countyEntryEnabled = locationMethod == types::LocationMethod::County; @@ -912,6 +958,18 @@ void SettingsDialogImpl::SetupAudioTab() self_->ui->resetAlertAudioLongitudeButton->setEnabled( coordinateEntryEnabled); + self_->ui->alertAudioRadarSiteComboBox->setEnabled( + radarSiteEntryEnable); + self_->ui->alertAudioRadarSiteSelectButton->setEnabled( + radarSiteEntryEnable); + self_->ui->resetAlertAudioRadarSiteButton->setEnabled( + radarSiteEntryEnable); + + self_->ui->alertAudioRadiusSpinBox->setEnabled( + radiusEntryEnable); + self_->ui->resetAlertAudioRadiusButton->setEnabled( + radiusEntryEnable); + self_->ui->alertAudioCountyLineEdit->setEnabled(countyEntryEnabled); self_->ui->alertAudioCountySelectButton->setEnabled( countyEntryEnabled); @@ -983,6 +1041,64 @@ void SettingsDialogImpl::SetupAudioTab() alertAudioLongitude_.SetResetButton( self_->ui->resetAlertAudioLongitudeButton); + alertAudioRadarSite_.SetSettingsVariable(audioSettings.alert_radar_site()); + alertAudioRadarSite_.SetMapFromValueFunction( + [](const std::string& id) -> std::string + { + // Get the radar site associated with the ID + std::shared_ptr radarSite = + config::RadarSite::Get(id); + + if (radarSite == nullptr) + { + // No radar site found, just return the ID + return id; + } + + // Add location details to the radar site + return RadarSiteLabel(radarSite); + }); + alertAudioRadarSite_.SetMapToValueFunction( + [](const std::string& text) -> std::string + { + // Find the position of location details + size_t pos = text.rfind(" ("); + + if (pos == std::string::npos) + { + // No location details found, just return the text + return text; + } + + // Remove location details from the radar site + return text.substr(0, pos); + }); + alertAudioRadarSite_.SetEditWidget(self_->ui->alertAudioRadarSiteComboBox); + alertAudioRadarSite_.SetResetButton( + self_->ui->resetAlertAudioRadarSiteButton); + UpdateAlertRadarDialogLocation(audioSettings.alert_radar_site().GetValue()); + + alertAudioRadius_.SetSettingsVariable(audioSettings.alert_radius()); + alertAudioRadius_.SetEditWidget(self_->ui->alertAudioRadiusSpinBox); + alertAudioRadius_.SetResetButton( + self_->ui->resetAlertAudioRadiusButton); + alertAudioRadius_.SetUnitLabel(self_->ui->alertAudioRadiusUnitsLabel); + auto alertAudioRadiusUpdateUnits = [this](const std::string& newValue) + { + types::DistanceUnits radiusUnits = + types::GetDistanceUnitsFromName(newValue); + double radiusScale = types::GetDistanceUnitsScale(radiusUnits); + std::string abbreviation = + types::GetDistanceUnitsAbbreviation(radiusUnits); + + alertAudioRadius_.SetUnit(radiusScale, abbreviation); + }; + settings::UnitSettings::Instance() + .distance_units() + .RegisterValueStagedCallback(alertAudioRadiusUpdateUnits); + alertAudioRadiusUpdateUnits( + settings::UnitSettings::Instance().distance_units().GetValue()); + auto& alertAudioPhenomena = types::GetAlertAudioPhenomena(); auto alertAudioLayout = static_cast(self_->ui->alertAudioGroupBox->layout()); @@ -1268,6 +1384,19 @@ void SettingsDialogImpl::UpdateRadarDialogLocation(const std::string& id) } } +void SettingsDialogImpl::UpdateAlertRadarDialogLocation(const std::string& id) +{ + std::shared_ptr radarSite = config::RadarSite::Get(id); + + if (radarSite != nullptr) + { + alertAudioRadarSiteDialog_->HandleMapUpdate(radarSite->latitude(), + radarSite->longitude()); + } +} + + + QFont SettingsDialogImpl::GetSelectedFont() { std::string fontFamily = fontFamilies_.at(selectedFontCategory_) diff --git a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui index 983d9b34..8d69be46 100644 --- a/scwx-qt/source/scwx/qt/ui/settings_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/settings_dialog.ui @@ -122,7 +122,7 @@ - 0 + 3 @@ -135,9 +135,9 @@ 0 - -113 + 0 511 - 669 + 647 @@ -610,8 +610,8 @@ 0 0 - 98 - 28 + 66 + 18 @@ -691,45 +691,6 @@ Alerts - - - - Latitude - - - - - - - ... - - - - :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg - - - - - - - Longitude - - - - - - - ... - - - - - - - Location Method - - - @@ -741,11 +702,19 @@ - - - - - :/res/icons/font-awesome-6/stop-solid.svg:/res/icons/font-awesome-6/stop-solid.svg + + + + 2 + + + 0.000000000000000 + + + 9999999999999.000000000000000 + + + 0.010000000000000 @@ -760,6 +729,23 @@ + + + + + 0 + 0 + + + + + + + + County + + + @@ -771,25 +757,10 @@ - - - - - :/res/icons/font-awesome-6/play-solid.svg:/res/icons/font-awesome-6/play-solid.svg - - - - - + + - Sound - - - - - - - County + @@ -809,6 +780,34 @@ + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + ... + + + + + + + ... + + + + + + @@ -825,21 +824,16 @@ - - - - - 0 - 0 - + + + + + :/res/icons/font-awesome-6/stop-solid.svg:/res/icons/font-awesome-6/stop-solid.svg - - - - - + + ... @@ -849,14 +843,21 @@ - - + + - ... + Latitude - + + + + Sound + + + + @@ -869,7 +870,15 @@ - + + + + + :/res/icons/font-awesome-6/play-solid.svg:/res/icons/font-awesome-6/play-solid.svg + + + + @@ -882,6 +891,66 @@ + + + + Location Method + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + + Radius + + + + + + + Longitude + + + + + + + Radar Site + + + + + + + ... + + + + + + + ... + + + + :/res/icons/font-awesome-6/rotate-left-solid.svg:/res/icons/font-awesome-6/rotate-left-solid.svg + + + + + + diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp index 89466975..bfaf408f 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.cpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.cpp @@ -5,7 +5,9 @@ #include #include +#include #include +#include namespace scwx { @@ -28,6 +30,40 @@ const ::GeographicLib::Geodesic& DefaultGeodesic() return geodesic_; } +bool GnomonicAreaContainsCenter(geos::geom::CoordinateSequence sequence) +{ + + // Cannot have an area with just two points + if (sequence.size() <= 2 || + (sequence.size() == 3 && sequence.front() == sequence.back())) + { + return false; + } + bool areaContainsPoint = false; + geos::geom::CoordinateXY zero {}; + // 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& ex) + { + logger_->warn("Invalid area sequence. {}", ex.what()); + } + } + + return areaContainsPoint; +} + bool AreaContainsPoint(const std::vector& area, const common::Coordinate& point) { @@ -82,6 +118,12 @@ bool AreaContainsPoint(const std::vector& area, return areaContainsPoint; } + + + + + + units::angle::degrees GetAngle(double lat1, double lon1, double lat2, double lon2) { @@ -137,6 +179,116 @@ GetDistance(double lat1, double lon1, double lat2, double lon2) return units::length::meters {distance}; } +/* + * Uses the gnomonic projection to determine if the area is in the radius. + * + * The basic algorithm is as follows: + * - Get a gnomonic projection centered on the point of the area + * - Find the point on the area which is closest to the center + * - Convert the closest point back to latitude and longitude. + * - Find the distance form the closest point to the point. + * + * The first property needed to make this work is that great circles become + * lines in the projection, which allows the area to be converted to strait + * lines. This is generally true for gnomic projections. + * + * The second property needed to make this work is that a point further away + * on the geodesic must be further away on the projection. This means that + * the closes point on the projection is also the closest point on the geodesic. + * This holds for spherical geodesics and is an approximation non spherical + * geodesics. + * + * This algorithm only works if the area is fully on the hemisphere centered + * on the point. Otherwise, this falls back to centroid based distances. + * + * If the point is inside the area, 0 is always returned. + */ +units::length::meters +GetDistanceAreaPoint(const std::vector& area, + const common::Coordinate& point) +{ + // Ensure that the same geodesic is used here as is for the distance + // calculation + ::GeographicLib::Gnomonic gnomonic = + ::GeographicLib::Gnomonic(DefaultGeodesic()); + geos::geom::CoordinateSequence sequence {}; + double x; + double y; + bool useCentroid = 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); + // Check if the current point is in the hemisphere centered on the point + // if not, fall back to using centroid. + if (std::isnan(x) || std::isnan(y)) + { + useCentroid = true; + } + sequence.add(x, y); + } + + units::length::meters distance; + + if (useCentroid) + { + common::Coordinate centroid = common::GetCentroid(area); + distance = GetDistance(point.latitude_, + point.longitude_, + centroid.latitude_, + centroid.longitude_); + } + else if (GnomonicAreaContainsCenter(sequence)) + { + distance = units::length::meters(0); + } + else + { + // Get the closes point on the geometry + auto geometryFactory = geos::geom::GeometryFactory::getDefaultInstance(); + auto lineString = geometryFactory->createLineString(sequence); + auto zeroPoint = geometryFactory->createPoint(zero); + + std::unique_ptr closestPoints = + geos::operation::distance::DistanceOp::nearestPoints(lineString.get(), + zeroPoint.get()); + + double closestLat; + double closestLon; + + gnomonic.Reverse(point.latitude_, + point.longitude_, + closestPoints->getX(0), + closestPoints->getY(0), + closestLat, + closestLon); + + distance = GetDistance(point.latitude_, + point.longitude_, + closestLat, + closestLon); + + } + return distance; +} + +bool AreaInRangeOfPoint(const std::vector& area, + const common::Coordinate& point, + const units::length::meters distance) +{ + return GetDistanceAreaPoint(area, point) <= distance; +} + } // namespace GeographicLib } // namespace util } // namespace qt diff --git a/scwx-qt/source/scwx/qt/util/geographic_lib.hpp b/scwx-qt/source/scwx/qt/util/geographic_lib.hpp index 30f7a63c..5038d9a9 100644 --- a/scwx-qt/source/scwx/qt/util/geographic_lib.hpp +++ b/scwx-qt/source/scwx/qt/util/geographic_lib.hpp @@ -90,6 +90,37 @@ common::Coordinate GetCoordinate(const common::Coordinate& center, units::length::meters GetDistance(double lat1, double lon1, double lat2, double lon2); +/** + * Get the distance from an area to a point. If the area is less than a quarter + * radius of the Earth away, this is the closest distance between the area and + * the point. Otherwise it is the distance from the centroid of the area to the + * point. Finally, if the point is in the area, it is always 0. + * + * @param [in] area A vector of Coordinates representing the area + * @param [in] point The point to check against the area + * + * @return true if area is inside the radius of the point + */ +units::length::meters +GetDistanceAreaPoint(const std::vector& area, + const common::Coordinate& point); + +/** + * Determine if an area/ring, oriented in either direction, is within a + * distance of a point. A point lying on the area boundary is considered to be + * inside the area, and thus always in range. Any part of the area being inside + * the radius counts as inside. Uses GetDistanceAreaPoint to get the distance. + * + * @param [in] area A vector of Coordinates representing the area + * @param [in] point The point to check against the area + * @param [in] distance The max distance in meters + * + * @return true if area is inside the radius of the point + */ +bool AreaInRangeOfPoint(const std::vector& area, + const common::Coordinate& point, + const units::length::meters distance); + } // namespace GeographicLib } // namespace util } // namespace qt diff --git a/test/data b/test/data index 0ea32947..51159935 160000 --- a/test/data +++ b/test/data @@ -1 +1 @@ -Subproject commit 0ea32947d6a6e39a69d931b2827056e7bc58cbba +Subproject commit 5115993596cfe6528b7a07c0b16dbd1ae16ce4df diff --git a/test/source/scwx/qt/util/geographic_lib.test.cpp b/test/source/scwx/qt/util/geographic_lib.test.cpp new file mode 100644 index 00000000..0052b028 --- /dev/null +++ b/test/source/scwx/qt/util/geographic_lib.test.cpp @@ -0,0 +1,69 @@ +#include + +#include +#include +#include + +namespace scwx +{ +namespace util +{ + +std::vector area = { + common::Coordinate(37.0193692, -91.8778413), + common::Coordinate(36.9719180, -91.3006973), + common::Coordinate(36.7270831, -91.6815753), +}; + +TEST(geographic_lib, area_in_range_inside) +{ + auto inside = common::Coordinate(36.9241584, -91.6425933); + bool value; + + // inside is always true + value = scwx::qt::util::GeographicLib::AreaInRangeOfPoint( + area, inside, units::length::meters(0)); + EXPECT_EQ(value, true); + value = scwx::qt::util::GeographicLib::AreaInRangeOfPoint( + area, inside, units::length::meters(1e6)); + EXPECT_EQ(value, true); +} + +TEST(geographic_lib, area_in_range_near) +{ + auto near = common::Coordinate(36.8009181, -91.3922700); + bool value; + + value = scwx::qt::util::GeographicLib::AreaInRangeOfPoint( + area, near, units::length::meters(9000)); + EXPECT_EQ(value, false); + value = scwx::qt::util::GeographicLib::AreaInRangeOfPoint( + area, near, units::length::meters(10100)); + EXPECT_EQ(value, true); + value = scwx::qt::util::GeographicLib::AreaInRangeOfPoint( + area, near, units::length::meters(1e6)); + EXPECT_EQ(value, true); +} + +TEST(geographic_lib, area_in_range_far) +{ + auto far = common::Coordinate(37.6481966, -94.2163834); + bool value; + + value = scwx::qt::util::GeographicLib::AreaInRangeOfPoint( + area, far, units::length::meters(9000)); + EXPECT_EQ(value, false); + value = scwx::qt::util::GeographicLib::AreaInRangeOfPoint( + area, far, units::length::meters(10100)); + EXPECT_EQ(value, false); + value = scwx::qt::util::GeographicLib::AreaInRangeOfPoint( + area, far, units::length::meters(100e3)); + EXPECT_EQ(value, false); + value = scwx::qt::util::GeographicLib::AreaInRangeOfPoint( + area, far, units::length::meters(300e3)); + EXPECT_EQ(value, true); +} + + +} // namespace util +} // namespace scwx diff --git a/test/test.cmake b/test/test.cmake index 0643c057..b3cfedd2 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -28,7 +28,8 @@ set(SRC_QT_MAP_TESTS source/scwx/qt/map/map_provider.test.cpp) set(SRC_QT_MODEL_TESTS source/scwx/qt/model/imgui_context_model.test.cpp) set(SRC_QT_SETTINGS_TESTS source/scwx/qt/settings/settings_container.test.cpp source/scwx/qt/settings/settings_variable.test.cpp) -set(SRC_QT_UTIL_TESTS source/scwx/qt/util/q_file_input_stream.test.cpp) +set(SRC_QT_UTIL_TESTS source/scwx/qt/util/q_file_input_stream.test.cpp + source/scwx/qt/util/geographic_lib.test.cpp) set(SRC_UTIL_TESTS source/scwx/util/float.test.cpp source/scwx/util/rangebuf.test.cpp source/scwx/util/streams.test.cpp