diff --git a/scwx-qt/res/icons/font-awesome-6/house-solid.svg b/scwx-qt/res/icons/font-awesome-6/house-solid.svg new file mode 100644 index 00000000..ee4c062e --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/house-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scwx-qt/res/icons/font-awesome-6/star-solid.svg b/scwx-qt/res/icons/font-awesome-6/star-solid.svg new file mode 100644 index 00000000..fd180e80 --- /dev/null +++ b/scwx-qt/res/icons/font-awesome-6/star-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scwx-qt/scwx-qt.qrc b/scwx-qt/scwx-qt.qrc index 53d7aec2..b9155f08 100644 --- a/scwx-qt/scwx-qt.qrc +++ b/scwx-qt/scwx-qt.qrc @@ -34,6 +34,7 @@ res/icons/font-awesome-6/forward-step-solid.svg res/icons/font-awesome-6/gears-solid.svg res/icons/font-awesome-6/github.svg + res/icons/font-awesome-6/house-solid.svg res/icons/font-awesome-6/layer-group-solid.svg res/icons/font-awesome-6/palette-solid.svg res/icons/font-awesome-6/pause-solid.svg @@ -44,6 +45,7 @@ res/icons/font-awesome-6/square-caret-right-regular.svg res/icons/font-awesome-6/square-minus-regular.svg res/icons/font-awesome-6/square-plus-regular.svg + res/icons/font-awesome-6/star-solid.svg res/icons/font-awesome-6/stop-solid.svg res/icons/font-awesome-6/volume-high-solid.svg res/palettes/wct/CC.pal diff --git a/scwx-qt/source/scwx/qt/main/main_window.cpp b/scwx-qt/source/scwx/qt/main/main_window.cpp index d94612be..d546b1fb 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.cpp +++ b/scwx-qt/source/scwx/qt/main/main_window.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -89,10 +90,7 @@ public: textEventManager_ {manager::TextEventManager::Instance()}, timelineManager_ {manager::TimelineManager::Instance()}, updateManager_ {manager::UpdateManager::Instance()}, - maps_ {}, - elevationCuts_ {}, - elevationButtonsChanged_ {false}, - resizeElevationButtons_ {false} + maps_ {} { mapProvider_ = map::GetMapProvider( settings::GeneralSettings::Instance().map_provider().GetValue()); @@ -129,6 +127,7 @@ public: } ~MainWindowImpl() { threadPool_.join(); } + void AddRadarSitePreset(const std::string& id); void AsyncSetup(); void ConfigureMapLayout(); void ConfigureMapStyles(); @@ -187,14 +186,15 @@ public: std::shared_ptr timelineManager_; std::shared_ptr updateManager_; + std::shared_ptr radarSiteModel_ { + model::RadarSiteModel::Instance()}; + std::map> radarSitePresetsActions_ {}; + QMenu* radarSitePresetsMenu_ {nullptr}; + std::vector maps_; - std::vector elevationCuts_; std::chrono::system_clock::time_point volumeTime_ {}; - bool elevationButtonsChanged_; - bool resizeElevationButtons_; - public slots: void UpdateMapParameters(double latitude, double longitude, @@ -213,10 +213,22 @@ MainWindow::MainWindow(QWidget* parent) : // Assign the bottom left corner to the left dock widget setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + // Configure Radar Site Box ui->vcpLabel->setVisible(false); ui->vcpValueLabel->setVisible(false); ui->vcpDescriptionLabel->setVisible(false); + p->radarSitePresetsMenu_ = new QMenu(this); + ui->radarSitePresetsButton->setMenu(p->radarSitePresetsMenu_); + + auto radarSitePresets = p->radarSiteModel_->presets(); + for (auto preset : radarSitePresets) + { + p->AddRadarSitePreset(preset); + } + + ui->radarSitePresetsButton->setVisible(!radarSitePresets.empty()); + // Configure Alert Dock p->alertDockWidget_ = new ui::AlertDockWidget(this); p->alertDockWidget_->setVisible(false); @@ -521,6 +533,19 @@ void MainWindow::on_actionAboutSupercellWx_triggered() p->aboutDialog_->show(); } +void MainWindow::on_radarSiteHomeButton_clicked() +{ + std::string homeRadarSite = + settings::GeneralSettings::Instance().default_radar_site().GetValue(); + + for (map::MapWidget* map : p->maps_) + { + map->SelectRadarSite(homeRadarSite); + } + + p->UpdateRadarSite(); +} + void MainWindow::on_radarSiteSelectButton_clicked() { p->radarSiteDialog_->show(); @@ -931,6 +956,27 @@ void MainWindowImpl::ConnectOtherSignals() UpdateRadarSite(); }); + connect(radarSiteModel_.get(), + &model::RadarSiteModel::PresetToggled, + [this](const std::string& siteId, bool isPreset) + { + if (isPreset && !radarSitePresetsActions_.contains(siteId)) + { + AddRadarSitePreset(siteId); + } + else if (!isPreset) + { + auto entry = radarSitePresetsActions_.find(siteId); + if (entry != radarSitePresetsActions_.cend()) + { + radarSitePresetsMenu_->removeAction(entry->second.get()); + radarSitePresetsActions_.erase(entry); + } + } + + mainWindow_->ui->radarSitePresetsButton->setVisible( + !radarSitePresetsActions_.empty()); + }); connect(updateManager_.get(), &manager::UpdateManager::UpdateAvailable, this, @@ -942,6 +988,40 @@ void MainWindowImpl::ConnectOtherSignals() }); } +void MainWindowImpl::AddRadarSitePreset(const std::string& siteId) +{ + auto radarSite = config::RadarSite::Get(siteId); + std::string actionText = + fmt::format("{}: {}", siteId, radarSite->location_name()); + + auto pair = radarSitePresetsActions_.emplace( + siteId, std::make_shared(QString::fromStdString(actionText))); + auto& action = pair.first->second; + + QAction* before = nullptr; + + // If the radar site is not at the end + if (pair.first != std::prev(radarSitePresetsActions_.cend())) + { + // Insert before the next entry in the list + before = std::next(pair.first)->second.get(); + } + + radarSitePresetsMenu_->insertAction(before, action.get()); + + connect(action.get(), + &QAction::triggered, + [this, siteId]() + { + for (map::MapWidget* map : maps_) + { + map->SelectRadarSite(siteId); + } + + UpdateRadarSite(); + }); +} + void MainWindowImpl::HandleFocusChange(QWidget* focused) { map::MapWidget* mapWidget = dynamic_cast(focused); diff --git a/scwx-qt/source/scwx/qt/main/main_window.hpp b/scwx-qt/source/scwx/qt/main/main_window.hpp index 82bda744..0ba8f41a 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.hpp +++ b/scwx-qt/source/scwx/qt/main/main_window.hpp @@ -46,6 +46,7 @@ private slots: void on_actionGitHubRepository_triggered(); void on_actionCheckForUpdates_triggered(); void on_actionAboutSupercellWx_triggered(); + void on_radarSiteHomeButton_clicked(); void on_radarSiteSelectButton_clicked(); private: diff --git a/scwx-qt/source/scwx/qt/main/main_window.ui b/scwx-qt/source/scwx/qt/main/main_window.ui index 56c659f3..4bd9b09c 100644 --- a/scwx-qt/source/scwx/qt/main/main_window.ui +++ b/scwx-qt/source/scwx/qt/main/main_window.ui @@ -39,7 +39,7 @@ 0 0 1024 - 22 + 21 @@ -141,8 +141,8 @@ 0 0 - 157 - 697 + 193 + 688 @@ -166,8 +166,126 @@ QFrame::Raised - + + + + + 0 + 0 + + + + KLSX + + + + + + + + 0 + 0 + + + + Volume Coverage Pattern + + + VCP + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 16777215 + 13 + + + + ... + + + + :/res/icons/font-awesome-6/house-solid.svg:/res/icons/font-awesome-6/house-solid.svg + + + + + + + + 16777215 + 13 + + + + ... + + + + :/res/icons/font-awesome-6/star-solid.svg:/res/icons/font-awesome-6/star-solid.svg + + + QToolButton::InstantPopup + + + + + + + + + + Radar Site + + + + + + + St. Louis, MO + + + + + + + 35 + + + + + + + Clear Air Mode + + + + @@ -180,51 +298,6 @@ - - - - St. Louis, MO - - - - - - - Volume Coverage Pattern - - - VCP - - - - - - - KLSX - - - - - - - Radar Site - - - - - - - Clear Air Mode - - - - - - - 35 - - - 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 ffca3638..66683ae3 100644 --- a/scwx-qt/source/scwx/qt/model/radar_site_model.cpp +++ b/scwx-qt/source/scwx/qt/model/radar_site_model.cpp @@ -2,9 +2,17 @@ #include #include #include +#include #include #include +#include + +#include +#include +#include +#include + namespace scwx { namespace qt @@ -15,36 +23,138 @@ namespace model static const std::string logPrefix_ = "scwx::qt::model::radar_site_model"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); -static constexpr size_t kColumnSiteId = 0u; -static constexpr size_t kColumnPlace = 1u; -static constexpr size_t kColumnState = 2u; -static constexpr size_t kColumnCountry = 3u; -static constexpr size_t kColumnLatitude = 4u; -static constexpr size_t kColumnLongitude = 5u; -static constexpr size_t kColumnType = 6u; -static constexpr size_t kColumnDistance = 7u; -static constexpr size_t kNumColumns = 8u; +static constexpr int kFirstColumn = + static_cast(RadarSiteModel::Column::SiteId); +static constexpr int kLastColumn = + static_cast(RadarSiteModel::Column::Preset); +static constexpr int kNumColumns = kLastColumn - kFirstColumn + 1; class RadarSiteModelImpl { public: - explicit RadarSiteModelImpl(); + explicit RadarSiteModelImpl() : + radarSites_ {}, + geodesic_(util::GeographicLib::DefaultGeodesic()), + distanceMap_ {}, + distanceDisplay_ {scwx::common::DistanceType::Miles}, + previousPosition_ {} + { + // Get all loaded radar sites + std::vector> radarSites = + config::RadarSite::GetAll(); + + // Setup radar site list + for (auto& site : radarSites) + { + distanceMap_[site->id()] = 0.0; + radarSites_.emplace_back(std::move(site)); + } + } ~RadarSiteModelImpl() = default; + void InitializePresets(); + void ReadPresets(); + void WritePresets(); + QList> radarSites_; + std::unordered_set presets_ {}; + + std::string presetsPath_ {}; 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"}; }; RadarSiteModel::RadarSiteModel(QObject* parent) : QAbstractTableModel(parent), p(std::make_unique()) { + p->InitializePresets(); + p->ReadPresets(); +} + +RadarSiteModel::~RadarSiteModel() +{ + // Write presets on shutdown + p->WritePresets(); +}; + +std::unordered_set RadarSiteModel::presets() const +{ + return p->presets_; +} + +void RadarSiteModelImpl::InitializePresets() +{ + std::string appDataPath { + QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + .toStdString()}; + + if (!std::filesystem::exists(appDataPath)) + { + if (!std::filesystem::create_directories(appDataPath)) + { + logger_->error("Unable to create application data directory: \"{}\"", + appDataPath); + } + } + + presetsPath_ = appDataPath + "/radar-presets.json"; +} + +void RadarSiteModelImpl::ReadPresets() +{ + logger_->info("Reading presets"); + + boost::json::value presetsJson = nullptr; + + // Determine if presets exists + if (std::filesystem::exists(presetsPath_)) + { + presetsJson = util::json::ReadJsonFile(presetsPath_); + } + + // If presets was successfully read + if (presetsJson != nullptr && presetsJson.is_array()) + { + auto& presetsArray = presetsJson.as_array(); + for (auto& presetsEntry : presetsArray) + { + if (presetsEntry.is_string()) + { + // Get radar site ID from JSON value + std::string preset = + boost::json::value_to(presetsEntry); + boost::to_upper(preset); + + // Find the preset in the list of radar sites + auto it = std::find_if( + radarSites_.cbegin(), + radarSites_.cend(), + [&preset](const std::shared_ptr& radarSite) + { return (radarSite->id() == preset); }); + + // If a match, add to the presets + if (it != radarSites_.cend()) + { + presets_.insert(preset); + } + } + } + } +} + +void RadarSiteModelImpl::WritePresets() +{ + logger_->info("Saving presets"); + + auto presetsJson = boost::json::value_from(presets_); + util::json::WriteJsonFile(presetsPath_, presetsJson); } -RadarSiteModel::~RadarSiteModel() = default; int RadarSiteModel::rowCount(const QModelIndex& parent) const { @@ -53,28 +163,32 @@ int RadarSiteModel::rowCount(const QModelIndex& parent) const int RadarSiteModel::columnCount(const QModelIndex& parent) const { - return parent.isValid() ? 0 : static_cast(kNumColumns); + return parent.isValid() ? 0 : kNumColumns; } QVariant RadarSiteModel::data(const QModelIndex& index, int role) const { - if (index.isValid() && index.row() >= 0 && - index.row() < p->radarSites_.size() && - (role == Qt::DisplayRole || role == types::SortRole)) + if (!index.isValid() || index.row() < 0 || + index.row() >= p->radarSites_.size()) { - const auto& site = p->radarSites_.at(index.row()); + return QVariant(); + } + const auto& site = p->radarSites_.at(index.row()); + + if (role == Qt::DisplayRole || role == types::SortRole) + { switch (index.column()) { - case kColumnSiteId: + case static_cast(Column::SiteId): return QString::fromStdString(site->id()); - case kColumnPlace: + case static_cast(Column::Place): return QString::fromStdString(site->place()); - case kColumnState: + case static_cast(Column::State): return QString::fromStdString(site->state()); - case kColumnCountry: + case static_cast(Column::Country): return QString::fromStdString(site->country()); - case kColumnLatitude: + case static_cast(Column::Latitude): if (role == Qt::DisplayRole) { return QString::fromStdString( @@ -84,7 +198,7 @@ QVariant RadarSiteModel::data(const QModelIndex& index, int role) const { return site->latitude(); } - case kColumnLongitude: + case static_cast(Column::Longitude): if (role == Qt::DisplayRole) { return QString::fromStdString( @@ -94,9 +208,9 @@ QVariant RadarSiteModel::data(const QModelIndex& index, int role) const { return site->longitude(); } - case kColumnType: + case static_cast(Column::Type): return QString::fromStdString(site->type_name()); - case kColumnDistance: + case static_cast(Column::Distance): if (role == Qt::DisplayRole) { if (p->distanceDisplay_ == scwx::common::DistanceType::Miles) @@ -116,6 +230,26 @@ QVariant RadarSiteModel::data(const QModelIndex& index, int role) const { return p->distanceMap_.at(site->id()); } + case static_cast(Column::Preset): + if (role == types::SortRole) + { + return QVariant(p->presets_.contains(site->id())); + } + break; + default: + break; + } + } + else if (role == Qt::DecorationRole) + { + switch (index.column()) + { + case static_cast(Column::Preset): + if (p->presets_.contains(site->id())) + { + return p->starIcon_; + } + break; default: break; } @@ -134,27 +268,40 @@ QVariant RadarSiteModel::headerData(int section, { switch (section) { - case kColumnSiteId: + case static_cast(Column::SiteId): return tr("Site ID"); - case kColumnPlace: + case static_cast(Column::Place): return tr("Place"); - case kColumnState: + case static_cast(Column::State): return tr("State"); - case kColumnCountry: + case static_cast(Column::Country): return tr("Country"); - case kColumnLatitude: + case static_cast(Column::Latitude): return tr("Latitude"); - case kColumnLongitude: + case static_cast(Column::Longitude): return tr("Longitude"); - case kColumnType: + case static_cast(Column::Type): return tr("Type"); - case kColumnDistance: + case static_cast(Column::Distance): return tr("Distance"); default: break; } } } + else if (role == Qt::DecorationRole) + { + if (orientation == Qt::Horizontal) + { + switch (section) + { + case static_cast(Column::Preset): + return p->starIcon_; + default: + break; + } + } + } return QVariant(); } @@ -175,31 +322,54 @@ void RadarSiteModel::HandleMapUpdate(double latitude, double longitude) p->distanceMap_[site->id()] = distanceInMeters; } - QModelIndex topLeft = createIndex(0, kColumnDistance); - QModelIndex bottomRight = createIndex(rowCount() - 1, kColumnDistance); + QModelIndex topLeft = createIndex(0, static_cast(Column::Distance)); + QModelIndex bottomRight = + createIndex(rowCount() - 1, static_cast(Column::Distance)); Q_EMIT dataChanged(topLeft, bottomRight); } -RadarSiteModelImpl::RadarSiteModelImpl() : - radarSites_ {}, - geodesic_(util::GeographicLib::DefaultGeodesic()), - distanceMap_ {}, - distanceDisplay_ {scwx::common::DistanceType::Miles}, - previousPosition_ {} +void RadarSiteModel::TogglePreset(int row) { - // Get all loaded radar sites - std::vector> radarSites = - config::RadarSite::GetAll(); - - // Setup radar site list - for (auto& site : radarSites) + if (row >= 0 && row < p->radarSites_.size()) { - distanceMap_[site->id()] = 0.0; - radarSites_.emplace_back(std::move(site)); + std::string siteId = p->radarSites_.at(row)->id(); + bool isPreset = false; + + // Attempt to erase the radar site from presets + if (p->presets_.erase(siteId) == 0) + { + // If the radar site did not exist, add it + p->presets_.insert(siteId); + isPreset = true; + } + + QModelIndex index = createIndex(row, static_cast(Column::Preset)); + Q_EMIT dataChanged(index, index); + + Q_EMIT PresetToggled(siteId, isPreset); } } +std::shared_ptr RadarSiteModel::Instance() +{ + static std::weak_ptr radarSiteModelReference_ {}; + static std::mutex instanceMutex_ {}; + + std::unique_lock lock(instanceMutex_); + + std::shared_ptr radarSiteModel = + radarSiteModelReference_.lock(); + + if (radarSiteModel == nullptr) + { + radarSiteModel = std::make_shared(); + radarSiteModelReference_ = radarSiteModel; + } + + return radarSiteModel; +} + } // namespace model } // namespace qt } // namespace scwx diff --git a/scwx-qt/source/scwx/qt/model/radar_site_model.hpp b/scwx-qt/source/scwx/qt/model/radar_site_model.hpp index 520be1b7..73e3d425 100644 --- a/scwx-qt/source/scwx/qt/model/radar_site_model.hpp +++ b/scwx-qt/source/scwx/qt/model/radar_site_model.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include @@ -15,10 +16,27 @@ class RadarSiteModelImpl; class RadarSiteModel : public QAbstractTableModel { + Q_OBJECT + public: + enum class Column : int + { + SiteId = 0, + Place = 1, + State = 2, + Country = 3, + Latitude = 4, + Longitude = 5, + Type = 6, + Distance = 7, + Preset = 8 + }; + explicit RadarSiteModel(QObject* parent = nullptr); ~RadarSiteModel(); + std::unordered_set presets() const; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; @@ -29,6 +47,12 @@ public: int role = Qt::DisplayRole) const override; void HandleMapUpdate(double latitude, double longitude); + void TogglePreset(int row); + + static std::shared_ptr Instance(); + +signals: + void PresetToggled(const std::string& siteId, bool isPreset); private: std::unique_ptr p; diff --git a/scwx-qt/source/scwx/qt/ui/radar_site_dialog.cpp b/scwx-qt/source/scwx/qt/ui/radar_site_dialog.cpp index c7275b06..66b7c3cd 100644 --- a/scwx-qt/source/scwx/qt/ui/radar_site_dialog.cpp +++ b/scwx-qt/source/scwx/qt/ui/radar_site_dialog.cpp @@ -24,13 +24,13 @@ class RadarSiteDialogImpl public: explicit RadarSiteDialogImpl(RadarSiteDialog* self) : self_ {self}, - radarSiteModel_ {new model::RadarSiteModel(self_)}, proxyModel_ {new QSortFilterProxyModel(self_)}, + radarSiteModel_ {model::RadarSiteModel::Instance()}, mapPosition_ {}, mapUpdateDeferred_ {false}, selectedRadarSite_ {"?"} { - proxyModel_->setSourceModel(radarSiteModel_); + proxyModel_->setSourceModel(radarSiteModel_.get()); proxyModel_->setSortRole(types::SortRole); proxyModel_->setFilterCaseSensitivity(Qt::CaseInsensitive); proxyModel_->setFilterKeyColumn(-1); @@ -38,9 +38,10 @@ public: ~RadarSiteDialogImpl() = default; RadarSiteDialog* self_; - model::RadarSiteModel* radarSiteModel_; QSortFilterProxyModel* proxyModel_; + std::shared_ptr radarSiteModel_; + scwx::common::Coordinate mapPosition_; bool mapUpdateDeferred_; @@ -70,9 +71,22 @@ RadarSiteDialog::RadarSiteDialog(QWidget* parent) : p->proxyModel_, &QSortFilterProxyModel::setFilterWildcard); connect(ui->radarSiteView, - &QTreeView::doubleClicked, + &QAbstractItemView::doubleClicked, this, [this]() { Q_EMIT accept(); }); + connect(ui->radarSiteView, + &QAbstractItemView::pressed, + this, + [this](const QModelIndex& index) + { + QModelIndex selectedIndex = p->proxyModel_->mapToSource(index); + + if (selectedIndex.column() == + static_cast(model::RadarSiteModel::Column::Preset)) + { + p->radarSiteModel_->TogglePreset(selectedIndex.row()); + } + }); connect( ui->radarSiteView->selectionModel(), &QItemSelectionModel::selectionChanged, diff --git a/scwx-qt/source/scwx/qt/ui/radar_site_dialog.ui b/scwx-qt/source/scwx/qt/ui/radar_site_dialog.ui index c923054a..74eae33b 100644 --- a/scwx-qt/source/scwx/qt/ui/radar_site_dialog.ui +++ b/scwx-qt/source/scwx/qt/ui/radar_site_dialog.ui @@ -6,7 +6,7 @@ 0 0 - 576 + 627 550 @@ -16,6 +16,9 @@ + + QAbstractItemView::CurrentChanged|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + true